Browse Source

init commit from v1.110

TC pushbot 5 4 years ago
parent
commit
a93952b44d
100 changed files with 13042 additions and 22 deletions
  1. 40 20
      .gitignore
  2. 556 0
      AGI Documentation.md
  3. 102 0
      FUNCLIST.md
  4. 117 0
      Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router.md
  5. 160 2
      README.md
  6. 73 0
      agi.go
  7. 29 0
      apt.go
  8. 114 0
      arsm.go
  9. 73 0
      auth.go
  10. 6 0
      autopush.bat
  11. 7 0
      autopush.sh
  12. 4 0
      autorelease.bat
  13. 48 0
      build.sh
  14. 1 0
      buildFullErrorList.bat
  15. 26 0
      build_binary-only.sh
  16. 74 0
      cluster.go
  17. 221 0
      common.go
  18. 246 0
      console.go
  19. 599 0
      desktop.go
  20. 41 0
      devices.go
  21. 137 0
      disk.go
  22. BIN
      documents/1.110 release drawing/celebrat_zhe.png
  23. BIN
      documents/1.110 release drawing/celebrat_zhe.psd
  24. BIN
      documents/1.110 release drawing/celebrate.png
  25. BIN
      documents/1.110 release drawing/celebrate.psd
  26. BIN
      documents/1.110 release drawing/fox.1.1.jpg
  27. BIN
      documents/1.110 release drawing/fox.1.1.png
  28. BIN
      documents/1.110 release drawing/fox.1.1.psd
  29. BIN
      documents/1.110 release drawing/fox.1.1_nobg.png
  30. BIN
      documents/1.110 release drawing/fox.1.1_nobg.psd
  31. BIN
      documents/1.110 release drawing/foxgirl.rar
  32. 0 0
      documents/AJGI structure.drawio
  33. BIN
      documents/AJGI structure.png
  34. 0 0
      documents/ArOZ Online OOP Structure rev 1
  35. BIN
      documents/ArOZ Online OOP Structure rev 1.png
  36. 0 0
      documents/ArOZ Online Subservice Architecture.drawio
  37. BIN
      documents/ArOZ Online Subservice Architecture.png
  38. 26 0
      documents/Feature Request and Bugs.txt
  39. 33 0
      documents/How to add aroz online service.txt
  40. 0 0
      documents/Modular File System Design
  41. BIN
      documents/Modular File System Design.png
  42. 0 0
      documents/Overall structure diagram.drawio
  43. BIN
      documents/Overall structure diagram.png
  44. BIN
      documents/Overall structure diagram~1.png
  45. 117 0
      documents/Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router.md
  46. 0 0
      documents/async and blocking upload mode.drawio
  47. BIN
      documents/async and blocking upload mode.png
  48. 186 0
      documents/common.go
  49. 1 0
      documents/dev.io/diagram 1.drawio
  50. BIN
      documents/dev.io/未命名圖表.png
  51. BIN
      documents/main logic folw.png
  52. BIN
      documents/mobile ui sketch.png
  53. 0 0
      documents/new oop structure
  54. 15 0
      documents/systemd service config example.txt
  55. 21 0
      documents/基本操作資訊 中文.md
  56. 20 0
      error.go
  57. 2451 0
      file_system.go
  58. BIN
      flag.txt
  59. 49 0
      go.mod
  60. 547 0
      go.sum
  61. 134 0
      hardware.power.go
  62. 23 0
      install-go.sh
  63. 248 0
      legacy/disabled/auth.go
  64. 47 0
      legacy/disabled/hardware.power.go
  65. 25 0
      legacy/disabled/language.go
  66. 326 0
      legacy/disabled/module.Music.go
  67. 114 0
      legacy/disabled/module.package.go
  68. 161 0
      legacy/disabled/network.ip2country.go
  69. 178 0
      legacy/disabled/system.boot.go
  70. 302 0
      legacy/disabled/system.disk.quota.go
  71. 381 0
      legacy/disabled/system.disk.smart.go
  72. 150 0
      legacy/disabled/system.disk.space.go
  73. 264 0
      legacy/disabled/system.permission.go
  74. 114 0
      legacy/disabled/system.power.go
  75. 150 0
      legacy/disabled/system.resetpw.go
  76. 237 0
      legacy/disabled/system.users.go
  77. 330 0
      legacy/module.Photo.go_disabled
  78. 138 0
      legacy/module.Video.go_disabled
  79. 25 0
      legacy/module.dummy.go_disabled
  80. 63 0
      legacy/module.notepadA.go_disabled
  81. 18 0
      localhost.crt
  82. 28 0
      localhost.key
  83. 92 0
      main.flags.go
  84. 141 0
      main.go
  85. 156 0
      main.router.go
  86. 151 0
      mediaServer.go
  87. 29 0
      mod/agi/agi.audio.go
  88. 585 0
      mod/agi/agi.file.go
  89. 313 0
      mod/agi/agi.go
  90. 204 0
      mod/agi/agi.http.go
  91. 267 0
      mod/agi/agi.image.go
  92. 227 0
      mod/agi/agi.ws.go
  93. 178 0
      mod/agi/common.go
  94. 34 0
      mod/agi/error.go
  95. 56 0
      mod/agi/static.go
  96. 352 0
      mod/agi/systemFunc.go
  97. 276 0
      mod/agi/userFunc.go
  98. 132 0
      mod/apt/apt.go
  99. 12 0
      mod/arsm/README.md
  100. 241 0
      mod/arsm/aecron/aecron.go

+ 40 - 20
.gitignore

@@ -1,26 +1,46 @@
-# ---> Go
-# Compiled Object files, Static and Dynamic libs (Shared Objects)
-*.o
-*.a
-*.so
+#OS stuffs
+.vscode/*
+.DS_Store
 
-# Folders
-_obj
-_test
+#Testing Folders
+test/deviceA/*
+test/deviceB/*
+tmp/*
+files/users*
+__debug_bin
+experimentals/*
 
-# Architecture specific extensions/prefixes
-*.[568vq]
-[568vq].out
+#Database related
+*.db
+system/db/*
+system/ao.db.lock
+*aofs.db.lock
+./web/aofs.db
 
-*.cgo1.go
-*.cgo2.c
-_cgo_defun.c
-_cgo_gotypes.go
-_cgo_export.*
+#Setting related
+system/network/wifi/ap/*
+system/storage.json
+system/dev.uuid
+system/storage.json
+system/storage/*.json
+system/cron.json
 
-_testmain.go
+#Logs related
+system/aecron/*.log
 
-*.exe
-*.test
-*.prof
+#Webapp related
+web/teleprompter/*
 
+#Subservice related
+subservice/*
+!subservice/ArSamba/*
+!subservice/demo/*
+!subservice/WsTTY/*
+
+#Binary related
+build/*
+aroz_online.exe
+aroz_online
+arozos.exe
+*/arozos.exe
+arozos

+ 556 - 0
AGI Documentation.md

@@ -0,0 +1,556 @@
+# AJGI Documentation
+
+## What is AJGI?
+AJGI is the shortform of ArOZ Javascript Gateway Interface.
+In simple words, you can add function to your system with JavaScript :)
+
+## Usages
+1. Put your js / agi file inside web/* (e.g. ./web/Dummy/backend/test.js)
+2. Load your script by calling / ajax request to ```/system/ajgi/interface?script={yourfile}.js```, (e.g. /system/ajgi/interface?script=Dummy/backend/test.js)
+3. Wait for the reponse from the script by calling sendResp in the script
+
+## Module Init Script
+To initialize a module without a main.go function call, you can create a "init.agi" script in your module root under ./web/myModule where "myModule" is your module name.
+
+To register the module, you can call to the "registerModule" function with JSON stringify module launch info following the following example JavaScript Object.
+
+```
+//Define the launchInfo for the module
+var moduleLaunchInfo = {
+	Name: "NotepadA",
+	Desc: "The best code editor on ArOZ Online",
+	Group: "Office",
+	IconPath: "NotepadA/img/module_icon.png",
+	Version: "1.2",
+	StartDir: "NotepadA/index.html",
+	SupportFW: true,
+	LaunchFWDir: "NotepadA/index.html",
+	SupportEmb: true,
+	LaunchEmb: "NotepadA/embedded.html",
+	InitFWSize: [1024, 768],
+	InitEmbSize: [360, 200],
+	SupportedExt: [".bat",".coffee",".cpp",".cs",".csp",".csv",".fs",".dockerfile",".go",".html",".ini",".java",".js",".lua",".mips",".md", ".sql",".txt",".php",".py",".ts",".xml",".yaml"]
+}
+
+//Register the module
+registerModule(JSON.stringify(moduleLaunchInfo));
+
+```
+
+You might also create the database table in this section of the code. For example:
+
+```
+//Create database for this module
+newDBTableIfNotExists("myModule")
+```
+
+## Application Examples
+See web/UnitTest/backend/*.js for more information on how to use AGI in webapps.
+
+For subservice, see subservice/demo/agi/ for more examples.
+
+
+### Access From Frontend
+To access server functions from front-end (e.g. You are building a serverless webapp on top of arozos), you can call to the ao_module.js function for running an agi script located under ```./web``` directory. You can find the ao_module.js wrapper under ```./web/script/```
+
+Here is an example extracted from Music module for listing files nearby the openeing music file.
+
+./web/Music/embedded.html
+```
+ao_module_agirun("Music/functions/getMeta.js", {
+	file: encodeURIComponent(playingFileInfo.filepath)
+}, function(data){
+	songList = data;
+	for (var i = 0; i < data.length; i++){
+		//Do something here
+	}
+});
+
+
+```
+
+./web/Music/functions/getMeta.js
+```
+//Define helper functions
+function bytesToSize(bytes) {
+    var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
+    if (bytes == 0) return '0 Byte';
+    var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
+    return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
+ }
+
+//Main Logic
+if (requirelib("filelib") == true){
+    //Get the filename from paramters
+    var openingFilePath = decodeURIComponent(file);
+    var dirname = openingFilePath.split("/")
+    dirname.pop()
+    dirname = dirname.join("/");
+
+    //Scan nearby files
+    var nearbyFiles = filelib.aglob(dirname + "/*") //aglob must be used here to prevent errors for non-unicode filename
+    var audioFiles = [];
+    var supportedFormats = [".mp3",".flac",".wav",".ogg",".aac",".webm",".mp4"];
+    //For each nearby files
+    for (var i =0; i < nearbyFiles.length; i++){
+        var thisFile = nearbyFiles[i];
+        var ext = thisFile.split(".").pop();
+        ext = "." + ext;
+        //Check if the file extension is in the supported extension list
+        for (var k = 0; k < supportedFormats.length; k++){
+            if (filelib.isDir(nearbyFiles[i]) == false && supportedFormats[k] == ext){
+                var fileExt = ext.substr(1);
+                var fileName = thisFile.split("/").pop();
+                var fileSize = filelib.filesize(thisFile);
+                var humanReadableFileSize = bytesToSize(fileSize);
+
+                var thisFileInfo = [];
+                thisFileInfo.push(fileName);
+                thisFileInfo.push(thisFile);
+                thisFileInfo.push(fileExt);
+                thisFileInfo.push(humanReadableFileSize);
+                
+                audioFiles.push(thisFileInfo);
+                break;
+            }
+        }
+    }
+    sendJSONResp(JSON.stringify(audioFiles));
+}
+
+```
+
+### Access from Subservice Backend
+It is also possible to access the AGI gateway from subservice backend.
+You can include aroz library from ```./subservice/demo/aroz``` . The following is an example extracted from demo subservice that request access to your desktop filelist.
+
+```
+package main
+import (
+	aroz "your/package/name/aroz"
+)
+
+var handler *aroz.ArozHandler
+
+//...
+
+func main(){
+	//Put other flags here
+
+	//Start subservice pipeline and flag parsing (This function call will also do flag.parse())
+	handler = aroz.HandleFlagParse(aroz.ServiceInfo{
+		Name: "Demo Subservice",
+		Desc: "A simple subservice code for showing how subservice works in ArOZ Online",			
+		Group: "Development",
+		IconPath: "demo/icon.png",
+		Version: "0.0.1",
+		//You can define any path before the actualy html file. This directory (in this case demo/ ) will be the reverse proxy endpoint for this module
+		StartDir: "demo/home.html",			
+		SupportFW: true, 
+		LaunchFWDir: "demo/home.html",
+		SupportEmb: true,
+		LaunchEmb: "demo/embedded.html",
+		InitFWSize: []int{720, 480},
+		InitEmbSize: []int{720, 480},
+		SupportedExt: []string{".txt",".md"},
+	});
+
+	//Start Web server with handler.Port
+	http.ListenAndServe(handler.Port, nil)
+}
+
+
+//Access AGI Gateway from Golang
+func agiGatewayTest(w http.ResponseWriter, r *http.Request){
+	//Get username and token from request
+	username, token := handler.GetUserInfoFromRequest(w,r)
+	log.Println("Received request from: ", username, " with token: ", token)
+
+	//Create an AGI Call that get the user desktop files
+	script := `
+		if (requirelib("filelib")){
+			var filelist = filelib.glob("user:/Desktop/*")
+			sendJSONResp(JSON.stringify(filelist));
+		}else{
+			sendJSONResp(JSON.stringify({
+				error: "Filelib require failed"
+			}));
+		}
+	`
+
+	//Execute the AGI request on server side
+	resp,err := handler.RequestGatewayInterface(token, script)
+	if err != nil{
+		//Something went wrong when performing POST request
+		log.Println(err)
+	}else{
+		//Try to read the resp body
+		bodyBytes, err := ioutil.ReadAll(resp.Body)
+		if err != nil{
+			log.Println(err)
+			w.Write([]byte(err.Error()))
+			return
+		}
+		resp.Body.Close()
+
+		//Relay the information to the request using json header
+		//Or you can process the information within the go program
+		w.Header().Set("Content-Type", "application/json")
+		w.Write(bodyBytes)
+
+	}
+}
+
+```
+
+
+
+## APIs
+
+### Basics
+#### Response to request
+In order for the script to return something to the screen / caller as JSON / TEXT response, 
+one of these functions has to be called.
+
+```
+sendResp(string)	=> Response header with text/plain header
+sendJSONResp(json_string) => Response request with JSON header
+```
+
+Customize header:
+
+You can also use customized header in return string as follow.
+
+```
+//Set Response header to html
+HTTP_HEADER = "text/html; charset=utf-8";
+
+//Send Response
+sendResp("<p>你好世界!</p>");
+```
+
+#### Register Module to module list
+You can call to the following function to register your module to the system module list. It is recommended that you register your module during the startup process (in the init.agi script located in your module root)
+
+Example Usage: 
+```
+registerModule(JSON.stringify(moduleInfo));
+
+```
+
+Module Info defination
+```
+//DO NOT USE THIS IN CODE. THIS IS A DATATYPE REPRESENTATION ONLY
+//PLEASE SEE THE INIT SECTION FOR A REAL OBJECT EXAMPLE
+moduleInfo = {
+	Name string					//Name of this module. e.g. "Audio"
+	Desc string					//Description for this module
+	Group string				//Group of the module, e.g. "system" / "media" etc
+	IconPath string				//Module icon image path e.g. "Audio/img/function_icon.png"
+	Version string				//Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
+	StartDir string 			//Default starting dir, e.g. "Audio/index.html"
+	SupportFW bool 				//Support floatWindow. If yes, floatWindow dir will be loaded
+	LaunchFWDir string 			//This link will be launched instead of 'StartDir' if fw mode
+	SupportEmb bool				//Support embedded mode
+	LaunchEmb string 			//This link will be launched instead of StartDir / Fw if a file is opened with this module
+	InitFWSize [int, int] 		//Floatwindow init size. [0] => Width, [1] => Height
+	InitEmbSize [int, int]		//Embedded mode init size. [0] => Width, [1] => Height
+	SupportedExt string_array 	//Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
+}
+```
+
+#### Print to STDOUT (console)
+To print something for debug, you can print text directly to ArOZ Online Core terminal using
+
+```
+console.log("text");
+```
+
+It has the same effect as using fmt.Println in golang.
+
+#### Delayed operations
+For delayed / timer ticking operations like setTimeout or setInterval is currently not supported.
+
+### System Functions
+System Functions are AGI functions that can be called anytime (system startup / scheduled task and user request tasks)
+The following variables and functions are categorized as system functions.
+
+#### CONST
+```
+BUILD_VERSION
+INTERNVAL_VERSION
+LOADED_MODULES
+LOADED_STORAGES
+```
+#### VAR
+```
+HTTP_RESP
+HTTP_HEADER (Default: "text/plain")
+```
+
+You can set HTTP_RESP with HTTP_HEADER to create custom response headers.
+For example, you can serve an HTML file using agi gateway
+```
+HTTP_RESP = "<html><body>Hello World</body></html>";
+HTTP_HEADER = "text/html";
+```
+
+#### Response Handlers
+```
+sendResp("Any string");
+sendJSONResp(JSON.stringify({text: "Hello World"));	//aka send Resp with JSON header
+
+```
+
+#### Database Related
+```
+newDBTableIfNotExists("tablename");
+dropDBTable("tablename");
+writeDBItem("tablename", "key", "value");
+readDBItem("tablename", "key");
+listDBTable("tablename"); //Return key value array
+deleteDBItem("tablename", "key");
+```
+
+#### Register and Packages
+```
+registerModule(JSON.stringify(moduleLaunchInfo)); //See moduleLaunchInfo in the sections above
+requirepkg("ffmpeg");
+execpkg("ffmpeg",'-i "files/users/TC/Desktop/群青.mp3" "files/users/TC/Desktop/群青.flac'); //ffmpeg must be required() before use
+
+```
+
+#### Structure & OOP
+```
+includes("hello world.js"); //Include another js / agi file within the current running one, return false if failed
+```
+
+### User Functions
+Users function are function group that only be usable when the interface is started from a user request.
+
+#### CONST
+```
+USERNAME
+USERICON
+USERQUOTA_TOTAL
+USERQUOTA_USED
+
+//Since AGI 1.3
+USER_VROOTS
+USER_MODULES //Might return ["*"] for admin permission
+```
+
+#### Filepath Virutalization
+```
+decodeVirtualPath("user:/Desktop"); //Convert virtual path (e.g. user:/Desktop) to real path (e.g. ./files/user/username/Desktop)
+decodeAbsoluteVirtualPath("user:/Desktop"); //Same as decodeVirtualPath but return in absolute path instead of relative path from the arozos binary root
+encodeRealPath("files/users/User/Desktop"); //Convert realpath into virtual path
+```
+
+#### Permission Related
+```
+getUserPermissionGroup();
+userIsAdmin(); => Return true / false
+```
+
+#### User Creation, Edit and Removal
+All the command in this section require administrator permission. To check if user is admin, use ``` userIsAdmin() ```.
+
+```
+userExists(username);
+createUser(username, password, defaultGroup);	//defaultGroup must be one of the permission group that exists in the system
+removeUser(username); //Return true if success, false if failed
+```
+
+#### Library requirement
+You can request other library to be loaded and have extra functions to work with files / images.
+```
+requirelib("filelib");
+```
+
+### filelib
+filelib is the core library for users to interact with the local filesystem.
+
+To use any of the library, the agi script must call the requirelib before calling any filelib functions. Example as follows.
+```
+
+if (!requirelib("filelib")){
+	console.log("Filelib import failed");
+}else{
+	console.log(filelib.fileExists("user:/Desktop/"));
+}
+```
+
+#### Filelib functions
+```
+	filelib.writeFile("user:/Desktop/test.txt", "Hello World"); 		//Write to file
+	filelib.readFile("user:/Desktop/test.txt");							//Read from file
+	filelib.readdir("user:/Desktop/"); 									//List all subdirectories within this directory
+	filelib.walk("user:/Desktop/"); 									//Recursive scan dir and return all files and folder in subdirs
+	filelib.glob("user:/Desktop/*.jpg");
+	filelib.aglob("user:/Desktop/*.jpg");
+	filelib.filesize("user:/Desktop/test.jpg");
+	filelib.fileExists("user:/Desktop/test.jpg");
+	filelib.isDir("user:/Desktop/NewFolder/");
+	filelib.md5("user:/Desktop/test.jpg");
+	filelib.mkdir("user/Desktop/NewFolder");	
+	filelib.mtime("user:/Desktop/test.jpg"); 							//Get modification time, return unix timestamp
+	filelib.rname("user:/Deskop"); 										//Get Rootname, return "User"
+```
+
+
+### imagelib
+A basic image handling library to process images. Allowing basic image resize,
+get image dimension and others (to be expanded)
+
+
+```
+//Include the library
+requirelib("imagelib");
+```
+
+#### ImageLib functions
+```
+imagelib.getImageDimension("user:/Desktop/test.jpg"); 									//return {width, height}
+imagelib.resizeImage("user:/Desktop/input.png", "user:/Desktop/output.png", 500, 300); 	//Resize input.png to 500 x 300 pixal and write to output.png
+imagelib.loadThumbString("user:/Desktop/test.jpg"); //Load the given file's thumbnail as base64 string, return false if failed
+imagelib.cropImage("user:/Desktop/test.jpg", "user:/Desktop/out.jpg",100,100,200,200)); 
+/*
+Crop the given image with the following arguemnts: 
+
+1) Input file (virtual path)
+2) Output file (virtual path, will be overwritten if exists)
+3) Position X
+4) Position Y
+5) Crop With
+6) Crop Height
+
+return true if success, false if failed
+*/
+
+
+```
+
+### http
+A basic http function group that allow GET / POST / HEAD / Download request to other web resources
+
+```
+//Include the library
+requirelib("http");
+```
+
+#### http functions
+```
+http.get("http://example.com/api/"); //Create a get request, return the respond body
+http.post("http://localhost:8080/system/file_system/listDir", JSON.stringify({
+    dir: "user:/Desktop",
+    sort: "default"
+}));	//Create a POST request with JSON payload
+http.head("http://localhost:8080/", "Content-Type"); //Get the header field "Content-Type" from the requested url, leave 2nd paramter empty to return the whole header in JSON string
+http.download("http://example.com/music.mp3", "user:/Desktop", "(Optional) My Music.mp3")
+
+```
+
+### websocket
+
+websocket library provide request upgrade from normal HTTP request to WebSocket connections. 
+
+```
+//Include the library
+requirelib("websocket");
+```
+
+#### websocket functions
+
+```
+websocket.upgrade(10); //Timeout value in integer, return false if failed
+var recv = websocket.read(); //Blocking websocket listen
+websocket.send("Hello World"); //Send websocket to client (web UI)
+websocket.close(); //Close websocket connection
+```
+
+
+
+#### Usage Example
+
+Font-end
+
+```
+function getWSEndpoint(){
+    //Open opeartion in websocket
+    let protocol = "wss://";
+    if (location.protocol !== 'https:') {
+    protocol = "ws://";
+    }
+    wsControlEndpoint = (protocol + window.location.hostname + ":" + window.location.port);
+    return wsControlEndpoint;
+}
+            
+let socket = new WebSocket(getWSEndpoint() + "/system/ajgi/interface?script=UnitTest/special/websocket.js");
+
+socket.onopen = function(e) {
+	log("✔️ Socket Opened");
+};
+
+socket.onmessage = function(event) {
+	log(`✔️ Received: ${event.data}`);
+};
+
+socket.onclose = function(event) {
+    if (event.wasClean) {
+    log(`📪 Connection Closed Cleanly code=${event.code} reason=${event.reason}`);
+    } else {
+    // e.g. server process killed or network down
+    // event.code is usually 1006 in this case
+    log(`❌ Connection Closed Unexpectedly`);
+    }
+};
+
+socket.onerror = function(error) {
+	log(`❌ ERROR! ${error.message}`);
+};
+```
+
+Backend example (without error handling). See the UnitTest/special/websocket.js for example with error handling.
+
+```
+
+function setup(){
+    //Require the WebSocket Library
+    requirelib("websocket");
+    websocket.upgrade(10);
+    console.log("WebSocket Opened!")
+    return true;
+}
+
+function waitForStart(){
+    websocket.send("Type something to start test");
+    var recv = websocket.read();
+    console.log(recv);
+}
+
+function loop(i){
+    websocket.send("Hello World: " + i);
+
+    //Wait for 1 second before next send
+    delay(1000);
+}
+
+function closing(){
+    //Try to close the WebSocket connection
+    websocket.close();
+}
+
+//Start executing the script
+if (setup()){
+    waitForStart();
+    for (var i = 0; i < 10; i++){
+        loop(i);
+    }
+    closing();
+}else{
+    console.log("WebSocket Setup Failed.")
+}
+
+```
+

+ 102 - 0
FUNCLIST.md

@@ -0,0 +1,102 @@
+# Developer Function List
+
+This is a list for quick referencing any functions for development
+
+## Main Functions (main.go)
+
+### Assistant Functions
+```
+mv(r *http.Request, getParamter string, postMode bool) => (string, error)
+sendTextResponse(w http.ResponseWriter, msg string)
+sendJSONResponse(w http.ResponseWriter, json string)
+```
+
+## Auth Related (auth.go)
+
+### Service Init
+```
+system_auth_service_init()
+```
+
+### Web Access Functions
+```
+system_auth_getIPAddress(w,r)
+system_auth_extCheckLogin(w,r)
+system_auth_register(w,r)
+system_auth_unregister(w,r)
+system_auth_login(w,r)
+system_auth_logout(w,r)
+
+```
+
+### Internal Access Functions
+```
+system_auth_chkauth(w,r) => bool
+system_auth_getUserName(w,r) => (string, error)
+system_auth_getUserCounts() => int
+system_auth_hash(raw string) => string
+
+```
+
+## Database (database.go)
+
+### Service Init
+```
+system_db_service_init(dbfile string) *skv.KVStore
+```
+
+### Web Access Functions
+```
+N/A
+```
+
+### Internal Access Functions
+```
+system_db_getValue(dbObject *skv.KVStore,key string) => (string, error)
+system_db_setValue(dbObject *skv.KVStore, key string, value string) => bool
+system_db_removeValue(dbObject *skv.KVStore, key string) => bool
+system_db_closeDatabase(dbObject *skv.KVStore)
+```
+
+## File System
+### Service Init
+```
+system_fs_service_init()
+```
+
+### Web Access Function
+```
+system_fs_validateFileOpr(w,r)
+system_fs_handleOpr(w,r)
+system_fs_handleList(w,r)
+system_fs_listRoot(w,r)
+system_fs_listDrives(w,r)
+system_fs_handleNewObjects(w,r)
+system_fs_handleUserPreference(w,r)
+system_fs_handleUpload(w,r)
+```
+
+### Internal Access Functions
+```
+//Use the following Glob function to replace filepath.Glob() for user upload directories, 
+//Example usage: system_fs_specialGlob("test/mydirectory/") !!CANNOT HANDLE *, RETURN ALL FILES IN DIR ONLY
+system_fs_specialGlob(path string)  => ([]string, error)
+
+//Use the following decode function to handle encodeURIComponent URL value
+system_fs_specialURIDecode(inputPath string) => string
+
+//Virtual and Realpath translation functions. Return error when permission denied or not exists
+virtualPathToRealPath(virtualPath string, username string) => (string, error)
+realpathToVirtualpath(realpath string, username string) => (string,error)
+
+//The following function return the rawsize, human readable filesize in float64 and its unit in string
+system_fs_getFileSize(path string) => (float64, float64, string, error)
+```
+
+
+## Others
+
+### SMART
+```
+ReadSMART() => (SMART)
+```

+ 117 - 0
Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router.md

@@ -0,0 +1,117 @@
+# Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router
+
+This is just a quick notes for myself on how to setup a Raspberry Pi 4 with Mercury AC650M USB WiFi Adapter
+
+### Problem
+
+The current setup of the system make use of a ARGON ONE metal case which, will make the build in WiFi adapter really hard to use as an AP. Hence, we need to setup an external WiFi adapter for this purpose.
+
+### Required Parts
+
+- Mercury USB WiFi Adapter AC650M (Dual baud 5G no driver version)
+- ARGON ONE metal case
+- Raspberry Pi 4B
+- 64GB Micro SD card
+
+
+
+### Installation
+
+1. Install Raspberry Pi OS and run apt-updates
+
+2. Download the driver for RTL8821CU
+
+   ```
+   mkdir -p ~/build
+   cd ~/build
+   git clone https://github.com/brektrou/rtl8821CU.git
+   ```
+
+   
+
+3. Install DKMS
+
+   ```
+   sudo apt-get install dkms
+   ```
+
+   
+
+4. Upgrade apt
+
+   ```
+   sudo apt update -y
+   sudo apt upgrade -y
+   ```
+
+5. Install bc and reboot
+
+   ```
+   sudo apt-get install bc
+   sudo reboot
+   ```
+
+6. Edit the Make file of the downloaded repo and change these two lines as follows
+
+   ```
+   CONFIG_PLATFORM_I386_PC = y
+   CONFIG_PLATFORM_ARM_RPI = n
+   ```
+
+   to
+
+   ```
+   CONFIG_PLATFORM_I386_PC = n
+   CONFIG_PLATFORM_ARM_RPI = y
+   ```
+
+7. Fix the compile flag on ARM processor
+
+   ```
+   sudo cp /lib/modules/$(uname -r)/build/arch/arm/Makefile /lib/modules/$(uname -r)/build/arch/arm/Makefile.$(date +%Y%m%d%H%M)
+   sudo sed -i 's/-msoft-float//' /lib/modules/$(uname -r)/build/arch/arm/Makefile
+   sudo ln -s /lib/modules/$(uname -r)/build/arch/arm /lib/modules/$(uname -r)/build/arch/armv7l
+   ```
+
+8. Build via DKMS
+
+   ```
+   sudo ./dkms-install.sh
+   ```
+
+   
+
+9. Plug your USB-wifi-adapter into your PC
+
+10. If wifi can be detected, congratulations. If not, maybe you need to switch your device usb mode by the following steps in terminal:
+
+    1. Find your usb-wifi-adapter device ID, like "0bda:1a2b", by type: ```lsusb```
+
+    2. Need install `usb_modeswitch` 
+
+       ```
+       sudo usb_modeswitch -KW -v 0bda -p 1a2b
+       systemctl start bluetooth.service
+       ```
+
+11.  Edit `usb_modeswitch` rules:
+
+    ```
+    udo nano /lib/udev/rules.d/40-usb_modeswitch.rules
+    ```
+
+12. Append before the end line `LABEL="modeswitch_rules_end"` the following:
+
+    ```
+    # Realtek 8211CU Wifi AC USB
+    ATTR{idVendor}=="0bda", ATTR{idProduct}=="1a2b", RUN+="/usr/sbin/usb_modeswitch -K -v 0bda -p 1a2b"
+    ```
+
+    
+
+
+
+
+
+
+

+ 160 - 2
README.md

@@ -1,3 +1,161 @@
-# arozos
+# ArOZ Online
 
-Final version of arozos, re-based for 1.111 and beyond. 
+This is the go implementation of ArOZ Online Web Desktop environment, perfect for Linux server usage.
+
+## Development Notes
+
+WIP
+
+## ArOZ JavaScript Gateway Interface / Plugin Loader
+The ArOZ AJGI / AGI interface provide a javascript programmable interface for ArOZ Online users to create 
+plugin for the system. To initiate the module, you can place a "init.agi" file in the web directory of the module
+(also named the module root). See more details in the [AJGI Documentation](AJGI Documentation.md).
+
+## Subservice Logics and Configuration
+To intergrate other binary based web server to the subservice interface,
+you can create a folder inside the "./subservice/your_service" where your binary
+executable should be named identically with the containing directory.
+For example, you have a module that provides web ui named "demo.exe",
+then your should put the demo.exe into "./subservice/demo/demo.exe".
+
+In the case of Linux environment, the subservice routine will first if the 
+module is installed via apt-get by checking with the "whereis" program.
+If the package is not found in the apt list, the binary of the program will be searched
+under the subservice directory.
+
+Please follow the naming convention given in the build.sh template.
+For example, the corrisponding platform will search for the corrisponding binary excutable filename:
+```
+demo_linux_amd64	=> Linux AMD64
+demo_linux_arm		=> Linux ARMv6l / v7l
+demo_linux_arm64	=> Linux ARM64
+demo_macOS_amd64	=> MacOS AMD64 (Not tested)
+```
+
+### Startup Flags
+During the tartup of the subservice, two paramters will be passed in. Here are the examples
+```
+demo.exe -info
+demo.exe -port 12810
+```
+
+In the case of reciving the "info" flag, the program should print the JSON string with correct module information
+as stated in the struct below.
+```
+//Struct for storing module information
+type serviecInfo struct{
+	Name string				//Name of this module. e.g. "Audio"
+	Desc string				//Description for this module
+	Group string			//Group of the module, e.g. "system" / "media" etc
+	IconPath string			//Module icon image path e.g. "Audio/img/function_icon.png"
+	Version string			//Version of the module. Format: [0-9]*.[0-9][0-9].[0-9]
+	StartDir string 		//Default starting dir, e.g. "Audio/index.html"
+	SupportFW bool 			//Support floatWindow. If yes, floatWindow dir will be loaded
+	LaunchFWDir string 		//This link will be launched instead of 'StartDir' if fw mode
+	SupportEmb bool			//Support embedded mode
+	LaunchEmb string 		//This link will be launched instead of StartDir / Fw if a file is opened with this module
+	InitFWSize []int 		//Floatwindow init size. [0] => Width, [1] => Height
+	InitEmbSize []int		//Embedded mode init size. [0] => Width, [1] => Height
+	SupportedExt []string 	//Supported File Extensions. e.g. ".mp3", ".flac", ".wav"
+}
+
+//Example Usage when reciving the -info flag
+infoObject := serviecInfo{
+		Name: "Demo Subservice",
+		Desc: "A simple subservice code for showing how subservice works in ArOZ Online",			
+		Group: "Development",
+		IconPath: "demo/icon.png",
+		Version: "0.0.1",
+		//You can define any path before the actualy html file. This directory (in this case demo/ ) will be the reverse proxy endpoint for this module
+		StartDir: "demo/home.html",			
+		SupportFW: true, 
+		LaunchFWDir: "demo/home.html",
+		SupportEmb: true,
+		LaunchEmb: "demo/embedded.html",
+		InitFWSize: []int{720, 480},
+		InitEmbSize: []int{720, 480},
+		SupportedExt: []string{".txt",".md"},
+	}
+	
+jsonString, _ := json.Marshal(info);
+fmt.Println(string(infoObject))
+os.Exit(0);
+```
+
+When reciving the port flag, the program should start the web ui at the given port. The following is an example for 
+the implementation of such functionality.
+
+```
+var port = flag.String("port", ":80", "The default listening endpoint for this subservice")
+flag.Parse()
+err := http.ListenAndServe(*port, nil)
+if err != nil {
+	log.Fatal(err)
+}
+```
+
+
+### Subservice Exec Settings
+In default, subservice routine will create a reverse proxy with URL rewrite build in that serve your web ui launched
+from the binary executable. If you do not need a reverse proxy connection, want a custom launch script or else, you can 
+use the following setting files.
+
+```
+.noproxy		=> Do not start a proxy to the given port
+.startscript	=> Send the launch parameter to the "start.bat" or "start.sh" file instead of the binary executable
+.suspended		=> Do not load this subservice during startup. But the user can enable it via the setting interface
+```
+
+Here is an example "start.bat" used in integrating Syncthing into ArOZ Online System with ".startscript" file placed next
+to the syncthing.exe file.
+```
+if not exist ".\config" mkdir ".\config"
+syncthing.exe -home=".\config" -no-browser -gui-address=127.0.0.1%2
+```
+
+## Systemd support
+To enable systemd in your host that support aroz online system, create a bash script at your aroz online root named "start.sh"
+and fill it up with your prefered startup paratmers. The most basic one is as follow:
+
+```
+#/bin/bash
+sudo ./aroz_online_linux_amd64
+
+```
+
+And then you can create a new file called "aroz-online.service" in /etc/systemd/system with the following contents (Assume your aroz online root is at /home/pi/aroz_online)
+
+```
+[Unit]
+Description=ArOZ Online Cloud Desktop Service.
+
+[Service]
+Type=simple
+WorkingDirectory=/home/pi/aroz_online/
+ExecStart=/bin/bash /home/pi/aroz_online/start.sh
+
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Finally to enable the service, use the following systemd commnads
+
+```
+#Enable the script during the startup process
+sudo systemctl enable aroz-online.service
+
+#Start the service now
+sudo systemctl start aroz-online.service
+
+#Show the status of the service
+systemctl status aroz-online.service
+
+
+#Disable the service if you no longer want it to start during boot
+sudo systemctl disable aroz-online.service
+```
+
+See more on how to use systemd over here: https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units

+ 73 - 0
agi.go

@@ -0,0 +1,73 @@
+package main
+
+import (
+	"log"
+	"net/http"
+
+	agi "imuslab.com/arozos/mod/agi"
+)
+
+var (
+	AGIGateway *agi.Gateway
+)
+
+func AGIInit() {
+	//Create new AGI Gateway object
+	gw, err := agi.NewGateway(agi.AgiSysInfo{
+		BuildVersion:         build_version,
+		InternalVersion:      internal_version,
+		LoadedModule:         moduleHandler.GetModuleNameList(),
+		ReservedTables:       []string{"auth", "permisson", "desktop"},
+		ModuleRegisterParser: moduleHandler.RegisterModuleFromJSON,
+		PackageManager:       packageManager,
+		UserHandler:          userHandler,
+		StartupRoot:          "./web",
+		ActivateScope:        []string{"./web", "./subservice"},
+		FileSystemRender:     thumbRenderHandler,
+	})
+	if err != nil {
+		log.Println("AGI Gateway Initialization Failed")
+	}
+
+	//Register user request handler endpoint
+	http.HandleFunc("/system/ajgi/interface", func(w http.ResponseWriter, r *http.Request) {
+		//Require login check
+		authAgent.HandleCheckAuth(w, r, func(w http.ResponseWriter, r *http.Request) {
+			//API Call from actual human users
+			thisuser, _ := gw.Option.UserHandler.GetUserInfoFromRequest(w, r)
+			gw.InterfaceHandler(w, r, thisuser)
+		})
+	})
+
+	//Register external API request handler endpoint
+	http.HandleFunc("/api/ajgi/interface", func(w http.ResponseWriter, r *http.Request) {
+		//Check if token exists
+		token, err := mv(r, "token", true)
+		if err != nil {
+			w.WriteHeader(http.StatusUnauthorized)
+			w.Write([]byte("401 - Unauthorized (token is empty)"))
+			return
+		}
+
+		//Validate Token
+		if authAgent.TokenValid(token) == true {
+			//Valid
+			thisUsername, err := gw.Option.UserHandler.GetAuthAgent().GetTokenOwner(token)
+			if err != nil {
+				log.Println(err)
+				w.WriteHeader(http.StatusInternalServerError)
+				w.Write([]byte("500 - Internal Server Error"))
+				return
+			}
+			thisuser, _ := gw.Option.UserHandler.GetUserInfoFromUsername(thisUsername)
+			gw.APIHandler(w, r, thisuser)
+		} else {
+			w.WriteHeader(http.StatusUnauthorized)
+			w.Write([]byte("401 - Unauthorized (Invalid / expired token)"))
+			return
+		}
+
+	})
+
+	AGIGateway = gw
+}

+ 29 - 0
apt.go

@@ -0,0 +1,29 @@
+package main
+
+import (
+	"net/http"
+	"log"
+	prout "imuslab.com/arozos/mod/prouter"
+	apt "imuslab.com/arozos/mod/apt"
+)
+
+func PackagManagerInit(){
+	//Create a package manager
+	packageManager = apt.NewPackageManager(*allow_package_autoInstall);
+	log.Println("Package Manager Initiated")
+
+	//Create a System Setting handler 
+	//aka who can access System Setting can see contents about packages
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName: "System Setting", 
+		AdminOnly: false, 
+		UserHandler: userHandler, 
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request){
+			sendErrorResponse(w, "Permission Denied");
+		},
+	});
+	
+	//Handle package listing request
+	router.HandleFunc("/system/apt/list", apt.HandlePackageListRequest)
+
+}

+ 114 - 0
arsm.go

@@ -0,0 +1,114 @@
+package main
+
+/*
+	ArOZ Remote Support and Management System
+	author: tobychui
+
+	This is a module for handling remote support and management of client
+	devices from other side of the network (even behind NAT)
+
+	This is a collection of submodules. Refer to the corrisponding submodules for more information
+*/
+
+import (
+	"log"
+	"net/http"
+
+	"imuslab.com/arozos/mod/arsm/aecron"
+	module "imuslab.com/arozos/mod/modules"
+	prout "imuslab.com/arozos/mod/prouter"
+)
+
+var (
+	cronObject *aecron.Aecron
+)
+
+func ArsmInit() {
+	/*
+		System Scheudler
+
+		The internal scheudler for arozos
+	*/
+	//Create an user router and its module
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "Tasks Scheduler",
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	//Register the module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:        "Tasks Scheduler",
+		Group:       "System Tools",
+		IconPath:    "SystemAO/arsm/img/scheduler.png",
+		Version:     "1.0",
+		StartDir:    "SystemAO/arsm/scheduler.html",
+		SupportFW:   true,
+		InitFWSize:  []int{1080, 580},
+		LaunchFWDir: "SystemAO/arsm/scheduler.html",
+		SupportEmb:  false,
+	})
+
+	//Startup the ArOZ Emulated Crontab Service
+	obj, err := aecron.NewArozEmulatedCrontab(userHandler, AGIGateway, "system/cron.json")
+	if err != nil {
+		log.Println("ArOZ Emulated Cron Startup Failed. Stopping all scheduled tasks.")
+	}
+
+	cronObject = obj
+
+	//Register Endpoints
+	http.HandleFunc("/system/arsm/aecron/list", func(w http.ResponseWriter, r *http.Request) {
+		if authAgent.CheckAuth(r) {
+			//User logged in
+			obj.HandleListJobs(w, r)
+		} else {
+			//User not logged in
+			http.NotFound(w, r)
+		}
+	})
+	router.HandleFunc("/system/arsm/aecron/add", obj.HandleAddJob)
+	router.HandleFunc("/system/arsm/aecron/remove", obj.HandleJobRemoval)
+	router.HandleFunc("/system/arsm/aecron/listlog", obj.HandleShowLog)
+
+	//Register settings
+	registerSetting(settingModule{
+		Name:         "Tasks Scheduler",
+		Desc:         "System Tasks and Excution Scheduler",
+		IconPath:     "SystemAO/arsm/img/small_icon.png",
+		Group:        "Cluster",
+		StartDir:     "SystemAO/arsm/aecron.html",
+		RequireAdmin: false,
+	})
+
+	/*
+		WsTerminal
+
+		The terminal that perform remote WebSocket based reverse ssh
+	*/
+	/*
+		wstRouter := prout.NewModuleRouter(prout.RouterOption{
+			ModuleName:  "System Setting",
+			AdminOnly:   true,
+			UserHandler: userHandler,
+			DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+				sendErrorResponse(w, "Permission Denied")
+			},
+		})
+
+		//Register settings
+		registerSetting(settingModule{
+			Name:         "WsTerminal",
+			Desc:         "Remote WebSocket Shell Terminal",
+			IconPath:     "SystemAO/arsm/img/wst.png",
+			Group:        "Cluster",
+			StartDir:     "SystemAO/arsm/wsterminal.html",
+			RequireAdmin: true,
+		})
+
+		log.Println("WebSocket Terminal, WIP: ", wstRouter)
+	*/
+}

+ 73 - 0
auth.go

@@ -0,0 +1,73 @@
+package main
+
+import (
+	"crypto/rand"
+	"log"
+	"net/http"
+
+	auth "imuslab.com/arozos/mod/auth"
+	prout "imuslab.com/arozos/mod/prouter"
+)
+
+func AuthInit() {
+	//Generate session key for authentication module if empty
+	sysdb.NewTable("auth")
+	if *session_key == "" {
+		//Check if the key was generated already. If not, generate a new one
+		if !sysdb.KeyExists("auth", "sessionkey") {
+			key := make([]byte, 32)
+			rand.Read(key)
+			newSessionKey := string(key)
+			sysdb.Write("auth", "sessionkey", newSessionKey)
+
+			log.Println("Authentication session key loaded from database")
+		} else {
+			log.Println("New authentication session key generated")
+		}
+		skeyString := ""
+		sysdb.Read("auth", "sessionkey", &skeyString)
+		session_key = &skeyString
+	}
+
+	//Create an Authentication Agent
+	authAgent = auth.NewAuthenticationAgent("ao_auth", []byte(*session_key), sysdb, *allow_public_registry, func(w http.ResponseWriter, r *http.Request) {
+		//Login Redirection Handler, redirect it login.system
+		w.Header().Set("Cache-Control", "no-cache, no-store, no-transform, must-revalidate, private, max-age=0")
+		http.Redirect(w, r, "/login.system?redirect="+r.URL.Path, 307)
+	})
+
+	if *allow_autologin == true {
+		authAgent.AllowAutoLogin = true
+	} else {
+		//Default is false. But just in case
+		authAgent.AllowAutoLogin = false
+	}
+
+	//Register the API endpoints for the authentication UI
+	authAgent.RegisterPublicAPIs(auth.AuthEndpoints{
+		Login:         "/system/auth/login",
+		Logout:        "/system/auth/logout",
+		Register:      "/system/auth/register",
+		CheckLoggedIn: "/system/auth/checkLogin",
+		Autologin:     "/api/auth/login",
+	})
+
+	authAgent.LoadAutologinTokenFromDB()
+
+}
+
+func AuthSettingsInit() {
+	//Authentication related settings
+	adminRouter := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "System Setting",
+		AdminOnly:   true,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	//Handle additional batch operations
+	adminRouter.HandleFunc("/system/auth/csvimport", authAgent.HandleCreateUserAccountsFromCSV)
+	adminRouter.HandleFunc("/system/auth/groupdel", authAgent.HandleUserDeleteByGroup)
+}

+ 6 - 0
autopush.bat

@@ -0,0 +1,6 @@
+@echo off
+set /p id="Enter commit notes: "
+git pull
+git add -A
+git commit -m "%id%"
+git push

+ 7 - 0
autopush.sh

@@ -0,0 +1,7 @@
+#!/bin/bash
+echo Enter commit notes:
+read commitmsg
+git pull
+git add *
+git commit -m "$commitmsg"
+git push

+ 4 - 0
autorelease.bat

@@ -0,0 +1,4 @@
+@echo off
+set /p id="Enter a release version (e.g. v0.0.1) :"
+git tag "%id%"
+git push origin "%id%"

+ 48 - 0
build.sh

@@ -0,0 +1,48 @@
+# /bin/sh
+echo "Building darwin"
+#GOOS=darwin GOARCH=386 go build
+#mv aroz_online build/aroz_online_macOS_i386
+GOOS=darwin GOARCH=amd64 go build
+mv arozos ../aroz_online_autorelease/arozos_darwin_amd64
+
+echo "Building linux"
+#GOOS=linux GOARCH=386 go build
+#mv aroz_online build/aroz_online_linux_i386
+GOOS=linux GOARCH=amd64 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_amd64
+GOOS=linux GOARCH=arm GOARM=6 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_arm
+GOOS=linux GOARCH=arm GOARM=7 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_armv7
+GOOS=linux GOARCH=arm64 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_arm64
+
+#Currently not CGO is required to build arozos. May remove dependencies later in the future
+#echo "Building OpenWRT"
+#GOOS=linux GOARCH=mipsle GOMIPS=softfloat CGO_ENABLED=0 go build
+#mv arozos ../aroz_online_autorelease/arozos_linux_mipsle
+
+echo "Building windows"
+#GOOS=windows GOARCH=386 go build
+#mv aroz_online.exe aroz_online_windows_i386.exe
+GOOS=windows GOARCH=amd64 go build
+mv arozos.exe ../aroz_online_autorelease/arozos_windows_amd64.exe
+
+echo "Removing old build resources"
+rm -rf ../aroz_online_autorelease/web/
+rm -rf ../aroz_online_autorelease/system/
+#rm -rf ../aroz_online_autorelease/subservice/
+
+echo "Moving subfolders to build folder"
+cp -r ./web ../aroz_online_autorelease/web/
+#cp -r ./subservice ../aroz_online_autorelease/subservice/
+cp -r ./system ../aroz_online_autorelease/system/
+
+rm ../aroz_online_autorelease/system/dev.uuid
+rm ../aroz_online_autorelease/system/ao.db
+mv ../aroz_online_autorelease/system/storage.json ../aroz_online_autorelease/system/storage.json.example
+rm -rf ../aroz_online_autorelease/system/aecron/
+rm ../aroz_online_autorelease/system/cron.json
+
+go build
+echo "Completed"

+ 1 - 0
buildFullErrorList.bat

@@ -0,0 +1 @@
+go build -gcflags="-e" >out.txt 2>&1

+ 26 - 0
build_binary-only.sh

@@ -0,0 +1,26 @@
+# /bin/sh
+echo "Building darwin"
+#GOOS=darwin GOARCH=386 go build
+#mv aroz_online build/aroz_online_macOS_i386
+GOOS=darwin GOARCH=amd64 go build
+mv arozos ../aroz_online_autorelease/arozos_darwin_amd64
+
+echo "Building linux"
+#GOOS=linux GOARCH=386 go build
+#mv aroz_online build/aroz_online_linux_i386
+GOOS=linux GOARCH=amd64 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_amd64
+GOOS=linux GOARCH=arm GOARM=6 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_arm
+GOOS=linux GOARCH=arm GOARM=7 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_armv7
+GOOS=linux GOARCH=arm64 go build
+mv arozos ../aroz_online_autorelease/arozos_linux_arm64
+
+echo "Building windows"
+#GOOS=windows GOARCH=386 go build
+#mv aroz_online.exe aroz_online_windows_i386.exe
+GOOS=windows GOARCH=amd64 go build
+mv arozos.exe ../aroz_online_autorelease/arozos_windows_amd64.exe
+
+echo "Completed"

+ 74 - 0
cluster.go

@@ -0,0 +1,74 @@
+package main
+
+import (
+	"log"
+	"net/http"
+
+	"imuslab.com/arozos/mod/cluster/aclient"
+	"imuslab.com/arozos/mod/network/neighbour"
+	prout "imuslab.com/arozos/mod/prouter"
+)
+
+/*
+	Functions related to ArozOS clusters
+	Author: tobychui
+
+	This is a section of the arozos core that handle cluster
+	related function endpoints
+
+*/
+
+var (
+	NeighbourDiscoverer *neighbour.Discoverer
+)
+
+func ClusterInit() {
+	//Only enable cluster scanning on mdns enabled mode
+	if *allow_mdns && MDNS != nil {
+		//Start the network discovery
+		thisDiscoverer := neighbour.NewDiscoverer(MDNS)
+		//Start a scan immediately (in go routine for non blocking)
+		go func() {
+			thisDiscoverer.UpdateScan(3)
+		}()
+
+		//Setup the scanning timer
+		thisDiscoverer.StartScanning(300, 5)
+		NeighbourDiscoverer = &thisDiscoverer
+
+		//Register the settings
+		registerSetting(settingModule{
+			Name:         "Neighbourhood",
+			Desc:         "Nearby ArOZ Host for Clustering",
+			IconPath:     "SystemAO/cluster/img/small_icon.png",
+			Group:        "Cluster",
+			StartDir:     "SystemAO/cluster/neighbour.html",
+			RequireAdmin: false,
+		})
+
+		//Register cluster scanning endpoints
+		router := prout.NewModuleRouter(prout.RouterOption{
+			ModuleName:  "System Setting",
+			UserHandler: userHandler,
+			DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+				errorHandlePermissionDenied(w, r)
+			},
+		})
+
+		router.HandleFunc("/system/cluster/scan", NeighbourDiscoverer.HandleScanningRequest)
+
+		/*
+			Start and Cluster Server and Client
+		*/
+
+		if *allow_clustering {
+			aclient.NewClient(aclient.AclientOption{
+				MDNS: MDNS,
+			})
+		}
+
+	} else {
+		log.Println("MDNS not enabled or startup failed. Skipping Cluster Scanner initiation.")
+	}
+
+}

+ 221 - 0
common.go

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

+ 246 - 0
console.go

@@ -0,0 +1,246 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"strings"
+)
+
+//Handle console command from the console module
+func consoleCommandHandler(input string) string {
+	//chunk := strings.Split(input, " ");
+	chunk, err := parseCommandLine(input)
+	if err != nil {
+		return err.Error()
+	}
+	if len(chunk) > 0 && chunk[0] == "auth" {
+		if matchSubfix(chunk, []string{"auth", "new"}, 4, "auth new {username} {password}") {
+			return "Creating a new user " + chunk[2] + " with password " + chunk[3]
+		} else if matchSubfix(chunk, []string{"auth", "dump"}, 4, "auth dump {filename}.csv") {
+			filename := chunk[2]
+			fmt.Println("Dumping user list to " + filename + " csv file")
+			csv := authAgent.ExportUserListAsCSV()
+			err := ioutil.WriteFile(filename, []byte(csv), 0755)
+			if err != nil {
+				return err.Error()
+			}
+			return "OK"
+		}
+	} else if len(chunk) > 0 && chunk[0] == "permission" {
+		if matchSubfix(chunk, []string{"permission", "list"}, 2, "") {
+			fmt.Println("> ", permissionHandler.PermissionGroups)
+			return "OK"
+		} else if matchSubfix(chunk, []string{"permission", "user"}, 3, "permission user {username}") {
+			username := chunk[2]
+			group, _ := permissionHandler.GetUsersPermissionGroup(username)
+			for _, thisGroup := range group {
+				fmt.Println(thisGroup)
+			}
+			return "OK"
+		} else if matchSubfix(chunk, []string{"permission", "group"}, 3, "permission group {groupname}") {
+			groupname := chunk[2]
+			groups := permissionHandler.PermissionGroups
+			for _, thisGroup := range groups {
+				if thisGroup.Name == groupname {
+					fmt.Println(thisGroup)
+				}
+			}
+			return "OK"
+		} else if matchSubfix(chunk, []string{"permission", "getinterface"}, 3, "permission getinterface {username}") {
+			//Get the list of interface module for this user
+			userinfo, err := userHandler.GetUserInfoFromUsername(chunk[2])
+			if err != nil {
+				return err.Error()
+			}
+			return strings.Join(userinfo.GetInterfaceModules(), ",")
+		}
+	} else if len(chunk) > 0 && chunk[0] == "quota" {
+		if matchSubfix(chunk, []string{"quota", "user"}, 3, "quota user {username}") {
+			userinfo, err := userHandler.GetUserInfoFromUsername(chunk[2])
+			if err != nil {
+				return err.Error()
+			}
+
+			fmt.Println("> "+"User Quota: ", userinfo.StorageQuota.UsedStorageQuota, "/", userinfo.StorageQuota.GetUserStorageQuota(), "bytes")
+			return "OK"
+		}
+	} else if len(chunk) > 0 && chunk[0] == "database" {
+		if matchSubfix(chunk, []string{"database", "dump"}, 3, "database dump {filename}") {
+			//Dump the database to file
+
+			return "WIP"
+		} else if matchSubfix(chunk, []string{"database", "list", "tables"}, 3, "") {
+			//List all opened tables
+			sysdb.Tables.Range(func(k, v interface{}) bool {
+				fmt.Println(k.(string))
+				return true
+			})
+			return "OK"
+		} else if matchSubfix(chunk, []string{"database", "view"}, 3, "database list {tablename}") {
+			//List everything in this table
+			tableList := []string{}
+
+			sysdb.Tables.Range(func(k, v interface{}) bool {
+				tableList = append(tableList, k.(string))
+				return true
+			})
+			if !inArray(tableList, chunk[2]) {
+				return "Table not exists"
+			} else if chunk[2] == "auth" {
+				return "You cannot view this database table"
+			}
+			entries, err := sysdb.ListTable(chunk[2])
+			if err != nil {
+				return err.Error()
+			}
+
+			for _, keypairs := range entries {
+				fmt.Println("> " + string(keypairs[0]) + ":" + string(keypairs[1]))
+			}
+
+			fmt.Println("Total Entry Count: ", len(entries))
+			return "OK"
+		}
+	} else if len(chunk) > 0 && chunk[0] == "user" {
+		if matchSubfix(chunk, []string{"user", "object", "dump"}, 4, "user object dump {username}") {
+			//Dump the given user object as json
+			userinfo, err := userHandler.GetUserInfoFromUsername(chunk[3])
+			if err != nil {
+				return err.Error()
+			}
+
+			jsonString, _ := json.Marshal(userinfo)
+			return string(jsonString)
+		} else if matchSubfix(chunk, []string{"user", "quota"}, 3, "user quota {username}") {
+			//List user quota of the given username
+			userinfo, err := userHandler.GetUserInfoFromUsername(chunk[2])
+			if err != nil {
+				return err.Error()
+			}
+
+			fmt.Println(userinfo.StorageQuota.UsedStorageQuota, "/", userinfo.StorageQuota.TotalStorageQuota)
+			return "OK"
+		}
+	} else if len(chunk) > 0 && chunk[0] == "storage" {
+		if matchSubfix(chunk, []string{"storage", "list", "basepool"}, 3, "") {
+			//Dump the base storage pool
+			jsonString, _ := json.Marshal(userHandler.GetStoragePool())
+			return string(jsonString)
+		}
+	} else if len(chunk) > 0 && chunk[0] == "find" {
+		if matchSubfix(chunk, []string{"find", "module"}, 3, "list module {modulename}") {
+			//Display all loaded modules
+			for _, module := range moduleHandler.LoadedModule {
+				if strings.ToLower(module.Name) == strings.ToLower(chunk[2]) {
+					jsonString, _ := json.Marshal(module)
+					return string(jsonString)
+				}
+			}
+			return string("Module not found")
+
+		} else if matchSubfix(chunk, []string{"find", "modules"}, 2, "") {
+			//Display all loaded modules
+			jsonString, _ := json.Marshal(moduleHandler.LoadedModule)
+			return string(jsonString)
+		} else if matchSubfix(chunk, []string{"find", "subservices"}, 2, "") {
+			//Display all loaded subservices
+			fmt.Println(ssRouter.RunningSubService)
+			return "OK"
+		}
+	} else if len(chunk) == 1 && chunk[0] == "stop" {
+		//Stopping the server
+		fmt.Println("Shutting down aroz online system by terminal request")
+		executeShutdownSequence()
+	}
+
+	return "Invalid Command. Given: '" + strings.Join(chunk, " ") + "'"
+}
+
+//Check if the given line input match the requirement
+func matchSubfix(chunk []string, match []string, minlength int, usageExample string) bool {
+	matching := true
+	//Check if the chunk contains minmium length of the command request
+	if len(chunk) >= len(match) {
+		for i, cchunk := range match {
+			if chunk[i] != cchunk {
+				matching = false
+			}
+		}
+	} else {
+		matching = false
+	}
+
+	if len(chunk)-minlength == -1 && chunk[len(chunk)-1] == match[len(match)-1] {
+		fmt.Println("Paramter missing. Usage: " + usageExample)
+		return false
+	}
+
+	return matching
+}
+
+func parseCommandLine(command string) ([]string, error) {
+	var args []string
+	state := "start"
+	current := ""
+	quote := "\""
+	escapeNext := true
+	for i := 0; i < len(command); i++ {
+		c := command[i]
+
+		if state == "quotes" {
+			if string(c) != quote {
+				current += string(c)
+			} else {
+				args = append(args, current)
+				current = ""
+				state = "start"
+			}
+			continue
+		}
+
+		if escapeNext {
+			current += string(c)
+			escapeNext = false
+			continue
+		}
+
+		if c == '\\' {
+			escapeNext = true
+			continue
+		}
+
+		if c == '"' || c == '\'' {
+			state = "quotes"
+			quote = string(c)
+			continue
+		}
+
+		if state == "arg" {
+			if c == ' ' || c == '\t' {
+				args = append(args, current)
+				current = ""
+				state = "start"
+			} else {
+				current += string(c)
+			}
+			continue
+		}
+
+		if c != ' ' && c != '\t' {
+			state = "arg"
+			current += string(c)
+		}
+	}
+
+	if state == "quotes" {
+		return []string{}, errors.New(fmt.Sprintf("Unclosed quote in command line: %s", command))
+	}
+
+	if current != "" {
+		args = append(args, current)
+	}
+
+	return args, nil
+}

+ 599 - 0
desktop.go

@@ -0,0 +1,599 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	module "imuslab.com/arozos/mod/modules"
+	prout "imuslab.com/arozos/mod/prouter"
+)
+
+//Desktop script initiation
+func DesktopInit() {
+	log.Println("Starting Desktop Services")
+
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "Desktop",
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	//Register all the required API
+	router.HandleFunc("/system/desktop/listDesktop", desktop_listFiles)
+	router.HandleFunc("/system/desktop/theme", desktop_theme_handler)
+	router.HandleFunc("/system/desktop/files", desktop_fileLocation_handler)
+	router.HandleFunc("/system/desktop/host", desktop_hostdetailHandler)
+	router.HandleFunc("/system/desktop/user", desktop_handleUserInfo)
+	router.HandleFunc("/system/desktop/preference", desktop_preference_handler)
+	router.HandleFunc("/system/desktop/createShortcut", desktop_shortcutHandler)
+
+	//API related to desktop based operations
+	router.HandleFunc("/system/desktop/opr/renameShortcut", desktop_handleShortcutRename)
+
+	//Initialize desktop database
+	err := sysdb.NewTable("desktop")
+	if err != nil {
+		log.Fatal(err)
+		os.Exit(1)
+	}
+
+	//Register Desktop Module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:        "Desktop",
+		Desc:        "The Web Desktop experience for everyone",
+		Group:       "Interface Module",
+		IconPath:    "img/desktop/desktop.png",
+		Version:     internal_version,
+		StartDir:    "",
+		SupportFW:   false,
+		LaunchFWDir: "",
+		SupportEmb:  false,
+	})
+}
+
+/////////////////////////////////////////////////////////////////////////////////////
+/*
+	FUNCTIONS RELATED TO PARSING DESKTOP FILE ICONS
+
+	The functions in this section handle file listing and its icon locations.
+*/
+func desktop_initUserFolderStructure(username string) {
+	//Call to filesystem for creating user file struture at root dir
+	userinfo, _ := userHandler.GetUserInfoFromUsername(username)
+	homedir, err := userinfo.GetHomeDirectory()
+	if err != nil {
+		log.Println(err)
+	}
+
+	if !fileExists(homedir + "Desktop") {
+		//Desktop directory not exists. Create one and copy a template desktop
+		os.MkdirAll(homedir+"Desktop", 0755)
+		templateFolder := "./system/desktop/template/"
+		if fileExists(templateFolder) {
+			templateFiles, _ := filepath.Glob(templateFolder + "*")
+			for _, tfile := range templateFiles {
+				input, _ := ioutil.ReadFile(tfile)
+				ioutil.WriteFile(homedir+"Desktop/"+filepath.Base(tfile), input, 0755)
+			}
+		}
+	}
+
+}
+
+//Return the information about the host
+func desktop_hostdetailHandler(w http.ResponseWriter, r *http.Request) {
+	type returnStruct struct {
+		Hostname        string
+		DeviceUUID      string
+		BuildVersion    string
+		InternalVersion string
+		DeviceVendor    string
+		DeviceModel     string
+		VendorIcon      string
+	}
+
+	jsonString, _ := json.Marshal(returnStruct{
+		Hostname:        *host_name,
+		DeviceUUID:      deviceUUID,
+		BuildVersion:    build_version,
+		InternalVersion: internal_version,
+		DeviceVendor:    deviceVendor,
+		DeviceModel:     deviceModel,
+		VendorIcon:      iconVendor,
+	})
+
+	sendJSONResponse(w, string(jsonString))
+}
+
+func desktop_handleShortcutRename(w http.ResponseWriter, r *http.Request) {
+	//Check if the user directory already exists
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	//Get the shortcut file that is renaming
+	target, err := mv(r, "src", false)
+	if err != nil {
+		sendErrorResponse(w, "Invalid shortcut file path given")
+		return
+	}
+
+	//Get the new name
+	new, err := mv(r, "new", false)
+	if err != nil {
+		sendErrorResponse(w, "Invalid new name given")
+		return
+	}
+
+	//Check if the file actually exists and it is on desktop
+	rpath, err := userinfo.VirtualPathToRealPath(target)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	if target[:14] != "user:/Desktop/" {
+		sendErrorResponse(w, "Shortcut not on desktop")
+		return
+	}
+
+	if !fileExists(rpath) {
+		sendErrorResponse(w, "File not exists")
+		return
+	}
+
+	//OK. Change the name of the shortcut
+	originalShortcut, err := ioutil.ReadFile(rpath)
+	if err != nil {
+		sendErrorResponse(w, "Shortcut file read failed")
+		return
+	}
+
+	lines := strings.Split(string(originalShortcut), "\n")
+	if len(lines) < 4 {
+		//Invalid shortcut properties
+		sendErrorResponse(w, "Invalid shortcut file")
+		return
+	}
+
+	//Change the 2nd line to the new name
+	lines[1] = new
+	newShortcutContent := strings.Join(lines, "\n")
+	err = ioutil.WriteFile(rpath, []byte(newShortcutContent), 0755)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	sendOK(w)
+}
+
+func desktop_listFiles(w http.ResponseWriter, r *http.Request) {
+	//Check if the user directory already exists
+	userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
+	username := userinfo.Username
+
+	//Initiate the user folder structure. Do nothing if the structure already exists.
+	desktop_initUserFolderStructure(username)
+
+	//List all files inside the user desktop directory
+	userDesktopRealpath, _ := userinfo.VirtualPathToRealPath("user:/Desktop/")
+	files, err := filepath.Glob(userDesktopRealpath + "/*")
+	if err != nil {
+		log.Fatal("Error. Desktop unable to load user files for :" + username)
+		return
+	}
+
+	//Desktop object structure
+	type desktopObject struct {
+		Filepath      string
+		Filename      string
+		Ext           string
+		IsDir         bool
+		IsEmptyDir    bool
+		IsShortcut    bool
+		ShortcutImage string
+		ShortcutType  string
+		ShortcutName  string
+		ShortcutPath  string
+		IconX         int
+		IconY         int
+	}
+
+	var desktopFiles []desktopObject
+	for _, this := range files {
+		//Always use linux convension for directory seperator
+		if filepath.Base(this)[:1] == "." {
+			//Skipping hidden files
+			continue
+		}
+		this = filepath.ToSlash(this)
+		thisFileObject := new(desktopObject)
+		thisFileObject.Filepath, _ = userinfo.RealPathToVirtualPath(this)
+		thisFileObject.Filename = filepath.Base(this)
+		thisFileObject.Ext = filepath.Ext(this)
+		thisFileObject.IsDir = IsDir(this)
+		if thisFileObject.IsDir {
+			//Check if this dir is empty
+			filesInFolder, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(this)) + "/*")
+			fc := 0
+			for _, f := range filesInFolder {
+				if filepath.Base(f)[:1] != "." {
+					fc++
+				}
+			}
+			if fc > 0 {
+				thisFileObject.IsEmptyDir = false
+			} else {
+				thisFileObject.IsEmptyDir = true
+			}
+		} else {
+			//File object. Default true
+			thisFileObject.IsEmptyDir = true
+		}
+		//Check if the file is a shortcut
+		isShortcut := false
+		if filepath.Ext(this) == ".shortcut" {
+			isShortcut = true
+			shortcutInfo, _ := ioutil.ReadFile(this)
+			infoSegments := strings.Split(strings.ReplaceAll(string(shortcutInfo), "\r\n", "\n"), "\n")
+			if len(infoSegments) < 4 {
+				thisFileObject.ShortcutType = "invalid"
+			} else {
+				thisFileObject.ShortcutType = infoSegments[0]
+				thisFileObject.ShortcutName = infoSegments[1]
+				thisFileObject.ShortcutPath = infoSegments[2]
+				thisFileObject.ShortcutImage = infoSegments[3]
+			}
+
+		}
+		thisFileObject.IsShortcut = isShortcut
+		//Check the file location
+		username, _ := authAgent.GetUserName(w, r)
+		x, y, _ := getDesktopLocatioFromPath(thisFileObject.Filename, username)
+		//This file already have a location on desktop
+		thisFileObject.IconX = x
+		thisFileObject.IconY = y
+
+		desktopFiles = append(desktopFiles, *thisFileObject)
+	}
+
+	//Convert the struct to json string
+	jsonString, _ := json.Marshal(desktopFiles)
+	sendJSONResponse(w, string(jsonString))
+}
+
+//functions to handle desktop icon locations. Location is directly written into the center db.
+func getDesktopLocatioFromPath(filename string, username string) (int, int, error) {
+	//As path include username, there is no different if there are username in the key
+	locationdata := ""
+	err := sysdb.Read("desktop", username+"/filelocation/"+filename, &locationdata)
+	if err != nil {
+		//The file location is not set. Return error
+		return -1, -1, errors.New("This file do not have a location registry")
+	}
+	type iconLocation struct {
+		X int
+		Y int
+	}
+	thisFileLocation := iconLocation{
+		X: -1,
+		Y: -1,
+	}
+	//Start parsing the from the json data
+	json.Unmarshal([]byte(locationdata), &thisFileLocation)
+	return thisFileLocation.X, thisFileLocation.Y, nil
+}
+
+//Set the icon location of a given filepath
+func setDesktopLocationFromPath(filename string, username string, x int, y int) error {
+	//You cannot directly set path of others people's deskop. Hence, fullpath needed to be parsed from auth username
+	userinfo, _ := userHandler.GetUserInfoFromUsername(username)
+	desktoppath, _ := userinfo.VirtualPathToRealPath("user:/Desktop/")
+	path := desktoppath + filename
+	type iconLocation struct {
+		X int
+		Y int
+	}
+
+	newLocation := new(iconLocation)
+	newLocation.X = x
+	newLocation.Y = y
+
+	//Check if the file exits
+	if fileExists(path) == false {
+		return errors.New("Given filename not exists.")
+	}
+
+	//Parse the location to json
+	jsonstring, err := json.Marshal(newLocation)
+	if err != nil {
+		log.Fatal("Unable to parse new file location on desktop for file: " + path)
+		return err
+	}
+
+	//log.Println(key,string(jsonstring))
+	//Write result to database
+	sysdb.Write("desktop", username+"/filelocation/"+filename, string(jsonstring))
+	return nil
+}
+
+func delDesktopLocationFromPath(filename string, username string) {
+	//Delete a file icon location from db
+	sysdb.Delete("desktop", username+"/filelocation/"+filename)
+}
+
+//Return the user information to the client
+func desktop_handleUserInfo(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	type returnStruct struct {
+		Username          string
+		UserIcon          string
+		UserGroups        []string
+		IsAdmin           bool
+		StorageQuotaTotal int64
+		StorageQuotaLeft  int64
+	}
+
+	//Calculate the storage quota left
+	remainingQuota := userinfo.StorageQuota.TotalStorageQuota - userinfo.StorageQuota.UsedStorageQuota
+	if userinfo.StorageQuota.TotalStorageQuota == -1 {
+		remainingQuota = -1
+	}
+
+	//Get the list of user permission group names
+	pgs := []string{}
+	for _, pg := range userinfo.GetUserPermissionGroup() {
+		pgs = append(pgs, pg.Name)
+	}
+
+	jsonString, _ := json.Marshal(returnStruct{
+		Username:          userinfo.Username,
+		UserIcon:          userinfo.GetUserIcon(),
+		IsAdmin:           userinfo.IsAdmin(),
+		UserGroups:        pgs,
+		StorageQuotaTotal: userinfo.StorageQuota.GetUserStorageQuota(),
+		StorageQuotaLeft:  remainingQuota,
+	})
+	sendJSONResponse(w, string(jsonString))
+}
+
+//Icon handling function for web endpoint
+func desktop_fileLocation_handler(w http.ResponseWriter, r *http.Request) {
+	get, _ := mv(r, "get", true) //Check if there are get request for a given filepath
+	set, _ := mv(r, "set", true) //Check if there are any set request for a given filepath
+	del, _ := mv(r, "del", true) //Delete the given filename coordinate
+
+	if set != "" {
+		//Set location with given paramter
+		x := 0
+		y := 0
+		sx, _ := mv(r, "x", true)
+		sy, _ := mv(r, "y", true)
+		path := set
+
+		x, err := strconv.Atoi(sx)
+		if err != nil {
+			x = 0
+		}
+
+		y, err = strconv.Atoi(sy)
+		if err != nil {
+			y = 0
+		}
+
+		//Set location of icon from path
+		username, _ := authAgent.GetUserName(w, r)
+		err = setDesktopLocationFromPath(path, username, x, y)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+		sendJSONResponse(w, string("\"OK\""))
+	} else if get != "" {
+		username, _ := authAgent.GetUserName(w, r)
+		x, y, _ := getDesktopLocatioFromPath(get, username)
+		result := []int{x, y}
+		json_string, _ := json.Marshal(result)
+		sendJSONResponse(w, string(json_string))
+	} else if del != "" {
+		username, _ := authAgent.GetUserName(w, r)
+		delDesktopLocationFromPath(del, username)
+	} else {
+		//No argument has been set
+		sendTextResponse(w, "Paramter missing.")
+	}
+}
+
+////////////////////////////////   END OF DESKTOP FILE ICON HANDLER ///////////////////////////////////////////////////
+
+func desktop_theme_handler(w http.ResponseWriter, r *http.Request) {
+	username, err := authAgent.GetUserName(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	//Check if the set GET paramter is set.
+	targetTheme, _ := mv(r, "set", false)
+	getUserTheme, _ := mv(r, "get", false)
+	if targetTheme == "" && getUserTheme == "" {
+		//List all the currnet themes in the list
+		themes, err := filepath.Glob("web/img/desktop/bg/*")
+		if err != nil {
+			log.Fatal("Error. Unable to search bg from destkop image root. Are you sure the web data folder exists?")
+			return
+		}
+		//Prase the results to json array
+		//Tips: You must use captial letter for varable in struct that is accessable as public :)
+		type desktopTheme struct {
+			Theme  string
+			Bglist []string
+		}
+
+		var desktopThemeList []desktopTheme
+		acceptBGFormats := []string{
+			".jpg",
+			".png",
+			".gif",
+		}
+		for _, file := range themes {
+			if IsDir(file) {
+				thisTheme := new(desktopTheme)
+				thisTheme.Theme = filepath.Base(file)
+				bglist, _ := filepath.Glob(file + "/*")
+				var thisbglist []string
+				for _, bg := range bglist {
+					ext := filepath.Ext(bg)
+					//if (sliceutil.Contains(acceptBGFormats, ext) ){
+					if stringInSlice(ext, acceptBGFormats) {
+						//This file extension is supported
+						thisbglist = append(thisbglist, filepath.Base(bg))
+					}
+
+				}
+				thisTheme.Bglist = thisbglist
+				desktopThemeList = append(desktopThemeList, *thisTheme)
+			}
+		}
+
+		//Return the results as JSON string
+		jsonString, err := json.Marshal(desktopThemeList)
+		if err != nil {
+			log.Fatal(err)
+		}
+		sendJSONResponse(w, string(jsonString))
+		return
+	} else if getUserTheme == "true" {
+		//Get the user's theme from database
+		result := ""
+		sysdb.Read("desktop", username+"/theme", &result)
+		if result == "" {
+			//This user has not set a theme yet. Use default
+			sendJSONResponse(w, string("\"default\""))
+			return
+		} else {
+			//This user already set a theme. Use its set theme
+			sendJSONResponse(w, string("\""+result+"\""))
+			return
+		}
+	} else if targetTheme != "" {
+		//Set the current user theme
+		sysdb.Write("desktop", username+"/theme", targetTheme)
+		sendJSONResponse(w, "\"OK\"")
+		return
+	}
+
+}
+
+func desktop_preference_handler(w http.ResponseWriter, r *http.Request) {
+	preferenceType, _ := mv(r, "preference", false)
+	value, _ := mv(r, "value", false)
+	username, err := authAgent.GetUserName(w, r)
+	if err != nil {
+		//user not logged in. Redirect to login page.
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+	if preferenceType == "" && value == "" {
+		//Invalid options. Return error reply.
+		sendTextResponse(w, "Error. Undefined paramter.")
+		return
+	} else if preferenceType != "" && value == "" {
+		//Getting config from the key.
+		result := ""
+		sysdb.Read("desktop", username+"/preference/"+preferenceType, &result)
+		jsonString, _ := json.Marshal(result)
+		sendJSONResponse(w, string(jsonString))
+		return
+	} else if preferenceType != "" && value != "" {
+		//Setting config from the key
+		sysdb.Write("desktop", username+"/preference/"+preferenceType, value)
+		sendJSONResponse(w, "\"OK\"")
+		return
+	} else {
+		sendTextResponse(w, "Error. Undefined paramter.")
+		return
+	}
+
+}
+
+func desktop_shortcutHandler(w http.ResponseWriter, r *http.Request) {
+	username, err := authAgent.GetUserName(w, r)
+
+	if err != nil {
+		//user not logged in. Redirect to login page.
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	userinfo, _ := userHandler.GetUserInfoFromUsername(username)
+
+	shortcutType, err := mv(r, "stype", true)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	shortcutText, err := mv(r, "stext", true)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	shortcutPath, err := mv(r, "spath", true)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	shortcutIcon, err := mv(r, "sicon", true)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	//OK to proceed. Generate a shortcut on the user desktop
+	userDesktopPath, _ := userinfo.VirtualPathToRealPath("user:/Desktop")
+	if !fileExists(userDesktopPath) {
+		os.MkdirAll(userDesktopPath, 0755)
+	}
+
+	//Check if there are desktop icon. If yes, override icon on module
+	if shortcutType == "module" && fileExists("./web/"+filepath.ToSlash(filepath.Dir(shortcutIcon)+"/desktop_icon.png")) {
+		shortcutIcon = filepath.ToSlash(filepath.Dir(shortcutIcon) + "/desktop_icon.png")
+	}
+
+	shortcutText = strings.ReplaceAll(shortcutText, "/", "")
+	for strings.Contains(shortcutText, "../") {
+		shortcutText = strings.ReplaceAll(shortcutText, "../", "")
+	}
+	shortcutFilename := userDesktopPath + "/" + shortcutText + ".shortcut"
+	counter := 1
+	for fileExists(shortcutFilename) {
+		shortcutFilename = userDesktopPath + "/" + shortcutText + "(" + IntToString(counter) + ")" + ".shortcut"
+		counter++
+	}
+	err = ioutil.WriteFile(shortcutFilename, []byte(shortcutType+"\n"+shortcutText+"\n"+shortcutPath+"\n"+shortcutIcon), 0755)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	sendOK(w)
+}

+ 41 - 0
devices.go

@@ -0,0 +1,41 @@
+package main
+
+import (
+
+)
+
+/*
+	Device Handler
+	
+	This script mainly handle the external devices like client devices reflect information
+	or IoT devices. If you want to handle storage devices mounting, use system.storage.go instead.
+*/
+
+func DeviceServiceInit(){
+	//Register Device related settings. Compatible to ArOZ Online Beta
+	registerSetting(settingModule{
+		Name:     "Client Device",
+		Desc:     "Detail about the browser you are using",
+		IconPath: "SystemAO/info/img/small_icon.png",
+		Group:    "Device",
+		StartDir: "SystemAO/info/clientInfo.html",
+	})
+
+	registerSetting(settingModule{
+		Name:     "Audio Testing",
+		Desc:     "Speaker and volume testing",
+		IconPath: "SystemAO/info/img/small_icon.png",
+		Group:    "Device",
+		StartDir: "SystemAO/info/audio.html",
+	})
+
+	registerSetting(settingModule{
+		Name:     "Display Testing",
+		Desc:     "Display testing tools",
+		IconPath: "SystemAO/info/img/small_icon.png",
+		Group:    "Device",
+		StartDir: "SystemAO/info/display.html",
+	})
+
+
+}

+ 137 - 0
disk.go

@@ -0,0 +1,137 @@
+package main
+
+/*
+	ArOZ Online Disk Service Endpoint Handler
+
+	This is a module to provide access to the disk services
+*/
+
+import (
+	"log"
+	"net/http"
+
+	"imuslab.com/arozos/mod/disk/diskmg"
+	diskspace "imuslab.com/arozos/mod/disk/diskspace"
+	smart "imuslab.com/arozos/mod/disk/smart"
+	sortfile "imuslab.com/arozos/mod/disk/sortfile"
+	prout "imuslab.com/arozos/mod/prouter"
+)
+
+func DiskServiceInit() {
+	//Register Disk Utilities under System Setting
+	//Disk info are only viewable by administrator
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "System Setting",
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	//Disk Space Display endpoint
+	router.HandleFunc("/system/disk/space/list", diskspace.HandleDiskSpaceList)
+
+	//New Large File Scanner
+	lfs := sortfile.NewLargeFileScanner(userHandler)
+	router.HandleFunc("/system/disk/space/largeFiles", lfs.HandleLargeFileList)
+
+	//Register settings
+	registerSetting(settingModule{
+		Name:         "Space Finder",
+		Desc:         "Reclaim Storage Space on Disks",
+		IconPath:     "SystemAO/disk/space/img/small_icon.png",
+		Group:        "Disk",
+		StartDir:     "SystemAO/disk/space/finder.html",
+		RequireAdmin: false,
+	})
+
+	if *allow_hardware_management {
+		//Displaying remaining space on disk, only enabled when allow hardware is true
+		registerSetting(settingModule{
+			Name:         "Disk Space",
+			Desc:         "System Storage Space on Disks",
+			IconPath:     "SystemAO/disk/space/img/small_icon.png",
+			Group:        "Disk",
+			StartDir:     "SystemAO/disk/space/diskspace.html",
+			RequireAdmin: false,
+		})
+	}
+
+	//Register Disk SMART services
+	if sudo_mode {
+		//Create a new admin router
+		adminRouter := prout.NewModuleRouter(prout.RouterOption{
+			ModuleName:  "System Setting",
+			AdminOnly:   true,
+			UserHandler: userHandler,
+			DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+				sendErrorResponse(w, "Permission Denied")
+			},
+		})
+
+		/*
+			SMART Listener
+			Handle disk SMART and disk information
+
+			See disk/SMART for more information
+		*/
+		if *allow_hardware_management {
+			smartListener, err := smart.NewSmartListener()
+			if err != nil {
+				//Listener creation failed
+				log.Println("Failed to create SMART listener: " + err.Error())
+			} else {
+				//Listener created. Register endpoints
+
+				//Register as a system setting
+				registerSetting(settingModule{
+					Name:         "Disk SMART",
+					Desc:         "HardDisk Health Checking",
+					IconPath:     "SystemAO/disk/smart/img/small_icon.png",
+					Group:        "Disk",
+					StartDir:     "SystemAO/disk/smart/smart.html",
+					RequireAdmin: true,
+				})
+
+				/*
+					registerSetting(settingModule{
+						Name:         "SMART Log",
+						Desc:         "HardDisk Health Log",
+						IconPath:     "SystemAO/disk/smart/img/small_icon.png",
+						Group:        "Disk",
+						StartDir:     "SystemAO/disk/smart/log.html",
+						RequireAdmin: true,
+					})
+				*/
+
+				adminRouter.HandleFunc("/system/disk/smart/getSMART", smartListener.GetSMART)
+				adminRouter.HandleFunc("/system/disk/smart/getSMARTTable", smartListener.CheckDiskTable)
+				adminRouter.HandleFunc("/system/disk/smart/getLogInfo", smartListener.CheckDiskTestStatus)
+			}
+		}
+
+		/*
+			Disk Manager Initialization
+			See disk/diskmg.go for more details
+
+			For setting register, see setting.advance.go
+		*/
+
+		if *allow_hardware_management {
+			adminRouter.HandleFunc("/system/disk/diskmg/view", diskmg.HandleView)
+			adminRouter.HandleFunc("/system/disk/diskmg/platform", diskmg.HandlePlatform)
+			adminRouter.HandleFunc("/system/disk/diskmg/mount", func(w http.ResponseWriter, r *http.Request) {
+				//Mount option require passing in all filesystem handlers
+				diskmg.HandleMount(w, r, fsHandlers)
+			})
+			adminRouter.HandleFunc("/system/disk/diskmg/format", func(w http.ResponseWriter, r *http.Request) {
+				//Format option require passing in all filesystem handlers
+				diskmg.HandleFormat(w, r, fsHandlers)
+			})
+			adminRouter.HandleFunc("/system/disk/diskmg/mpt", diskmg.HandleListMountPoints)
+		}
+
+	}
+
+}

BIN
documents/1.110 release drawing/celebrat_zhe.png


BIN
documents/1.110 release drawing/celebrat_zhe.psd


BIN
documents/1.110 release drawing/celebrate.png


BIN
documents/1.110 release drawing/celebrate.psd


BIN
documents/1.110 release drawing/fox.1.1.jpg


BIN
documents/1.110 release drawing/fox.1.1.png


BIN
documents/1.110 release drawing/fox.1.1.psd


BIN
documents/1.110 release drawing/fox.1.1_nobg.png


BIN
documents/1.110 release drawing/fox.1.1_nobg.psd


BIN
documents/1.110 release drawing/foxgirl.rar


File diff suppressed because it is too large
+ 0 - 0
documents/AJGI structure.drawio


BIN
documents/AJGI structure.png


File diff suppressed because it is too large
+ 0 - 0
documents/ArOZ Online OOP Structure rev 1


BIN
documents/ArOZ Online OOP Structure rev 1.png


File diff suppressed because it is too large
+ 0 - 0
documents/ArOZ Online Subservice Architecture.drawio


BIN
documents/ArOZ Online Subservice Architecture.png


+ 26 - 0
documents/Feature Request and Bugs.txt

@@ -0,0 +1,26 @@
+================== Feature Requests ==================
+- Samba control functions / setup interface
+- File Sharing Interface for user private files to public
+- Allow Music Module to playback m4a files
+- Upload Progress display on Desktop
+- Add omxplayer support (For local playback, e.g. omxplayer -b --vol -3000 {filename})
+
+================== Bugs ======================
+
+
+================== To Be Implemented ==================
+- Add photo processing functionality in agi gateway
+- Rewrite File System into modular based architecture
+- Context menu on shortcuts on Desktop
+- Context menu on floatWindow drag bar
+- Search input in System settings
+- Photo Module rewrite
+- Migrate AirMusic module to AGI script
+- Power Management Functions
+- Export user account information from command line
+
+
+================== Functions Already Implemented =============
+[OK] Add delete key on Desktop functions
+[OK] Try to reserach if it is possible to drag from Web Desktop to Window Desktop
+[OK] Able to focus windows on the back by clicking the float window object

+ 33 - 0
documents/How to add aroz online service.txt

@@ -0,0 +1,33 @@
+1. CD into systemd service folder
+	cd /etc/systemd/system/
+
+2. Create the file like arozos.service
+	sudo nano arozos.service
+
+3.1 Enable these two services
+	sudo systemctl enable systemd-networkd.service systemd-networkd-wait-online.service
+
+3.2. Write the service content as follow (replace your startup directories)
+
+[Unit]
+Description=AROZOS Cloud Desktop Service.
+After=systemd-networkd-wait-online.service
+Wants=systemd-networkd-wait-online.service
+
+[Service]
+Type=simple
+ExecStartPre=/bin/sleep 30
+WorkingDirectory=/home/pi/arozos/
+ExecStart=/bin/bash /home/pi/arozos/start.sh
+
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+
+4. Start the service with
+	sudo systemctl start arozos.service
+
+5. Make it automatically startup during boot
+	sudo systemctl enable arozos.service

File diff suppressed because it is too large
+ 0 - 0
documents/Modular File System Design


BIN
documents/Modular File System Design.png


File diff suppressed because it is too large
+ 0 - 0
documents/Overall structure diagram.drawio


BIN
documents/Overall structure diagram.png


BIN
documents/Overall structure diagram~1.png


+ 117 - 0
documents/Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router.md

@@ -0,0 +1,117 @@
+# Quick Notes for Setting Up Raspberry Pi 4 as WiFi Router
+
+This is just a quick notes for myself on how to setup a Raspberry Pi 4 with Mercury AC650M USB WiFi Adapter
+
+### Problem
+
+The current setup of the system make use of a ARGON ONE metal case which, will make the build in WiFi adapter really hard to use as an AP. Hence, we need to setup an external WiFi adapter for this purpose.
+
+### Required Parts
+
+- Mercury USB WiFi Adapter AC650M (Dual baud 5G no driver version)
+- ARGON ONE metal case
+- Raspberry Pi 4B
+- 64GB Micro SD card
+
+
+
+### Installation
+
+1. Install Raspberry Pi OS and run apt-updates
+
+2. Download the driver for RTL8821CU
+
+   ```
+   mkdir -p ~/build
+   cd ~/build
+   git clone https://github.com/brektrou/rtl8821CU.git
+   ```
+
+   
+
+3. Install DKMS
+
+   ```
+   sudo apt-get install dkms
+   ```
+
+   
+
+4. Upgrade apt
+
+   ```
+   sudo apt update -y
+   sudo apt upgrade -y
+   ```
+
+5. Install bc and reboot
+
+   ```
+   sudo apt-get install bc
+   sudo reboot
+   ```
+
+6. Edit the Make file of the downloaded repo and change these two lines as follows
+
+   ```
+   CONFIG_PLATFORM_I386_PC = y
+   CONFIG_PLATFORM_ARM_RPI = n
+   ```
+
+   to
+
+   ```
+   CONFIG_PLATFORM_I386_PC = n
+   CONFIG_PLATFORM_ARM_RPI = y
+   ```
+
+7. Fix the compile flag on ARM processor
+
+   ```
+   sudo cp /lib/modules/$(uname -r)/build/arch/arm/Makefile /lib/modules/$(uname -r)/build/arch/arm/Makefile.$(date +%Y%m%d%H%M)
+   sudo sed -i 's/-msoft-float//' /lib/modules/$(uname -r)/build/arch/arm/Makefile
+   sudo ln -s /lib/modules/$(uname -r)/build/arch/arm /lib/modules/$(uname -r)/build/arch/armv7l
+   ```
+
+8. Build via DKMS
+
+   ```
+   sudo ./dkms-install.sh
+   ```
+
+   
+
+9. Plug your USB-wifi-adapter into your PC
+
+10. If wifi can be detected, congratulations. If not, maybe you need to switch your device usb mode by the following steps in terminal:
+
+    1. Find your usb-wifi-adapter device ID, like "0bda:1a2b", by type: ```lsusb```
+
+    2. Need install `usb_modeswitch` 
+
+       ```
+       sudo usb_modeswitch -KW -v 0bda -p 1a2b
+       systemctl start bluetooth.service
+       ```
+
+11.  Edit `usb_modeswitch` rules:
+
+    ```
+    udo nano /lib/udev/rules.d/40-usb_modeswitch.rules
+    ```
+
+12. Append before the end line `LABEL="modeswitch_rules_end"` the following:
+
+    ```
+    # Realtek 8211CU Wifi AC USB
+    ATTR{idVendor}=="0bda", ATTR{idProduct}=="1a2b", RUN+="/usr/sbin/usb_modeswitch -K -v 0bda -p 1a2b"
+    ```
+
+    
+
+
+
+
+
+
+

File diff suppressed because it is too large
+ 0 - 0
documents/async and blocking upload mode.drawio


BIN
documents/async and blocking upload mode.png


+ 186 - 0
documents/common.go

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

+ 1 - 0
documents/dev.io/diagram 1.drawio

@@ -0,0 +1 @@
+<mxfile host="Electron" modified="2021-02-13T04:29:31.157Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.7.3 Chrome/85.0.4183.98 Electron/10.1.2 Safari/537.36" etag="Kd8_E2Fubsd6ZpeCKSGL" version="13.7.3" type="device"><diagram id="zf_OCXzsDWNsziVzxSpI" name="第1頁">7Vptj6M2EP41kdoPjcCEl3zcJJuu1Lt21T3prp9WTvCB7wAj42TJ/vrawQSwycttN1DSrrQRHuzBfp6Z8YxhZM3j/FcK0/Aj8VE0Aoafj6zFCAB3MuW/QrArBLbtFIKAYr8QmZXgCb8iKTSkdIN9lDU6MkIihtOmcE2SBK1ZQwYpJS/Nbl9J1HxqCgOkCZ7WMNKln7HPwkLqAbeSPyAchOWTTUcuOIZlZ7mSLIQ+eamJrPuRNaeEsOIqzucoEtiVuBTjlkfuHiZGUcIuGfDp7vn1JUS/fXuwCNnCB2Oe/P6LJCNju3LByOfrl01CWUgCksDovpLOKNkkPhJaDd6q+nwgJOVCkwu/IcZ2kky4YYSLQhZH8i6fMN19EePHdtn8q35vkUvlRWsnW8VcxQSPQiBFGdnQNTqx7tKUIA0QO9EPHIjiBo5IjPh8+DiKIsjwtjkPKE0tOPSr2OAXkpAfIEfq3cJoI5+ksYXjvf3W0ZWiBY4D/tQIr/jvOsLpM6SMXwYoQRTyZy/v45TtnpfcIxB9NoGX8/9xmgTyqYgylJ8GWgdGDrBK95Ve75RO8FL5kCNFYc19bONKULr92nndymtGf307BxfaufUP7Xw/9I5SuKt1SAlOWFbT/CgElZUAq2klNrAVoguNFe2Hqb3dEsBgnQoYClxOz05lDRZKNT7ZRs9Q2r3GJ7O3+DS5MD4dobObfXhy3s5rqArDxDyB/ABXKHokGWaYJPzeijBGYt4BRjgQgjVHC1EuiETPGVx/D/aUzklE6F6t9XX/V1N6J8cyQfEsY5R8P+Sl4CCpaTAMz1gK0g7pp2j4MAsPlsPvpGIZcR6IFH6MSeaOMU+os7FP1pt4T+rsHbzONtSswNa8zgK6102u5XWeRuyfIh0HRhF0NJb50lnThTQuVaJi7PuFt6IMv8LVXpVAXW6OXK89G9kLoYs7aFb4qvk+gJtACXOuDrjXEubAtQCfaoA/bVY1vA1z8Jhb6i5t61tLp5iXW91x0PVwNjjQ1XrD7Rt0vXZbYg6JOL3gPytxFZMtj8FDh37inofeNDrFXk/xH0XKCYxPFG73x1P5DcSZ6fkSu2Pc9XpAw/0moo1aiDkTfVvtGHo9RW2D3ro56NsKt46hdzToNZQHUgV7LalKp1Ww6Q4WS9Uw3WnfWOrFzVCwVO3StfrGUq9bbrtQ9IyeC8VS8X+pUnQ93cy7BV0vWm6+Upy2JBDdgj7RMO3x6Nft8t2UtKbzh79eO6XXfTulFrau4ylkX+PtlF67aubx/1nzJccSDmjuaHbPZ81Ar45v9UTIakLfFmK7rdHK9wynvAol/p34Xoi3EpIoWfGlUU/HpLZm+0TK+mOhTH+T7hzJ30oVRTCWoyo0NUVa6m0riopgrSl6t/h3QTV9kimUY/aldl17o8lb1aYmGuWe9i/4TqOsfM/vhU6vdqbsiZ75VjsD6ubasZ3pJw1/pCgRw/iPE4mAu+IZrxOIq884EfvZLRQd6jc3bsvZpdtiQNbVYrN+TlFjwjhBxvCLEZWMtiSlWzL0g46PPCdp5eGnBYUBv7OgJP158FRMXKfpFy3nd2114Ruo4M3qq98ioFWfTlv3fwM=</diagram></mxfile>

BIN
documents/dev.io/未命名圖表.png


BIN
documents/main logic folw.png


BIN
documents/mobile ui sketch.png


File diff suppressed because it is too large
+ 0 - 0
documents/new oop structure


+ 15 - 0
documents/systemd service config example.txt

@@ -0,0 +1,15 @@
+[Unit]
+Description=AROZOS Cloud Service
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=simple
+WorkingDirectory=/home/pi/aroz_online/
+ExecStart=/bin/bash /home/pi/aroz_online/start.sh
+
+Restart=always
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target

+ 21 - 0
documents/基本操作資訊 中文.md

@@ -0,0 +1,21 @@
+# 歡迎使用 ArOZ Online 1.0 預覽版雲作業系統
+### 好了啦廢話少說,有甚麼需要注意的地方?
+
+1. 這系統只能處理小型檔案,一般來說文件 / 音樂檔案等都可以放,影片也可以但是不太建議
+2. 因為這個仍然是 ArOZ Online 的內部測試版,部分功能可能還不能用
+3. 如果要更新的話可以用 ssh 連進去後台執行 aroz_online 裡面的 ./update.sh (記得 cd 進去)
+4. 對了,因為這東西實在真的太便宜了對於需要很多 CPU 處理能力的工作(好像 FFmpeg 轉檔工廠),你真的要給它一點時間,我指一點,是指十多分鐘 owo
+5. 嘛,用來做一般文書處理和放一下小檔案其實還算是不錯的啦wwww
+
+
+### 更新方法
+使用 Raspberry Pi 預設的帳戶登入後台(ssh)
+username: pi
+password: raspberry
+
+然後進入 aroz_online 的根目錄,執行更新檔案之後重新載入瀏覽器,需要的指令如下:
+
+```
+cd ./aroz_online
+./update.sh
+```

+ 20 - 0
error.go

@@ -0,0 +1,20 @@
+package main
+
+/*
+	This page mainly used to handle error interfaces
+
+*/
+
+import "net/http"
+
+func errorHandleNotFound(w http.ResponseWriter, r *http.Request) {
+	http.NotFound(w, r)
+}
+
+func errorHandleNotLoggedIn(w http.ResponseWriter, r *http.Request) {
+	http.Error(w, "Not authorized", 401)
+}
+
+func errorHandlePermissionDenied(w http.ResponseWriter, r *http.Request) {
+	http.Error(w, "Not authorized", 401)
+}

+ 2451 - 0
file_system.go

@@ -0,0 +1,2451 @@
+package main
+
+import (
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"io"
+	"io/ioutil"
+	"log"
+	"math"
+	"mime/multipart"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+	"runtime"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/gorilla/websocket"
+	uuid "github.com/satori/go.uuid"
+
+	fs "imuslab.com/arozos/mod/filesystem"
+	fsp "imuslab.com/arozos/mod/filesystem/fspermission"
+	hidden "imuslab.com/arozos/mod/filesystem/hidden"
+	metadata "imuslab.com/arozos/mod/filesystem/metadata"
+	module "imuslab.com/arozos/mod/modules"
+	prout "imuslab.com/arozos/mod/prouter"
+	"imuslab.com/arozos/mod/share"
+	storage "imuslab.com/arozos/mod/storage"
+	user "imuslab.com/arozos/mod/user"
+)
+
+var (
+	thumbRenderHandler *metadata.RenderHandler
+	shareManager       *share.Manager
+)
+
+type trashedFile struct {
+	Filename         string
+	Filepath         string
+	FileExt          string
+	IsDir            bool
+	Filesize         int64
+	RemoveTimestamp  int64
+	RemoveDate       string
+	OriginalPath     string
+	OriginalFilename string
+}
+
+func FileSystemInit() {
+	router := prout.NewModuleRouter(prout.RouterOption{
+		ModuleName:  "File Manager",
+		AdminOnly:   false,
+		UserHandler: userHandler,
+		DeniedHandler: func(w http.ResponseWriter, r *http.Request) {
+			sendErrorResponse(w, "Permission Denied")
+		},
+	})
+
+	//Upload related functions
+	router.HandleFunc("/system/file_system/upload", system_fs_handleUpload)
+	router.HandleFunc("/system/file_system/lowmemUpload", system_fs_handleLowMemoryUpload)
+
+	//Other file operations
+	router.HandleFunc("/system/file_system/validateFileOpr", system_fs_validateFileOpr)
+	router.HandleFunc("/system/file_system/fileOpr", system_fs_handleOpr)
+	router.HandleFunc("/system/file_system/ws/fileOpr", system_fs_handleWebSocketOpr)
+	router.HandleFunc("/system/file_system/listDir", system_fs_handleList)
+	router.HandleFunc("/system/file_system/listDirHash", system_fs_handleDirHash)
+	router.HandleFunc("/system/file_system/listRoots", system_fs_listRoot)
+	router.HandleFunc("/system/file_system/listDrives", system_fs_listDrives)
+	router.HandleFunc("/system/file_system/newItem", system_fs_handleNewObjects)
+	router.HandleFunc("/system/file_system/preference", system_fs_handleUserPreference)
+	router.HandleFunc("/system/file_system/listTrash", system_fs_scanTrashBin)
+	router.HandleFunc("/system/file_system/ws/listTrash", system_fs_WebSocketScanTrashBin)
+	router.HandleFunc("/system/file_system/clearTrash", system_fs_clearTrashBin)
+	router.HandleFunc("/system/file_system/restoreTrash", system_fs_restoreFile)
+	router.HandleFunc("/system/file_system/zipHandler", system_fs_zipHandler)
+	router.HandleFunc("/system/file_system/getProperties", system_fs_getFileProperties)
+	router.HandleFunc("/system/file_system/pathTranslate", system_fs_handlePathTranslate)
+	router.HandleFunc("/system/file_system/handleFileWrite", system_fs_handleFileWrite)
+	router.HandleFunc("/system/file_system/handleFilePermission", system_fs_handleFilePermission)
+	router.HandleFunc("/system/file_system/search", system_fs_handleFileSearch)
+
+	//Thumbnail caching functions
+	router.HandleFunc("/system/file_system/handleFolderCache", system_fs_handleFolderCache)
+	router.HandleFunc("/system/file_system/handleCacheRender", system_fs_handleCacheRender)
+
+	//Register the module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:        "File Manager",
+		Group:       "System Tools",
+		IconPath:    "SystemAO/file_system/img/small_icon.png",
+		Version:     "1.0",
+		StartDir:    "SystemAO/file_system/file_explorer.html",
+		SupportFW:   true,
+		InitFWSize:  []int{1080, 580},
+		LaunchFWDir: "SystemAO/file_system/file_explorer.html",
+		SupportEmb:  false,
+	})
+
+	//Register the Trashbin module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:         "Trash Bin",
+		Group:        "System Tools",
+		IconPath:     "SystemAO/file_system/trashbin_img/small_icon.png",
+		Version:      "1.0",
+		StartDir:     "SystemAO/file_system/trashbin.html",
+		SupportFW:    true,
+		InitFWSize:   []int{1080, 580},
+		LaunchFWDir:  "SystemAO/file_system/trashbin.html",
+		SupportEmb:   false,
+		SupportedExt: []string{"*"},
+	})
+
+	//Register the Zip Extractor module
+	moduleHandler.RegisterModule(module.ModuleInfo{
+		Name:         "Zip Extractor",
+		Group:        "System Tools",
+		IconPath:     "SystemAO/file_system/img/zip_extractor.png",
+		Version:      "1.0",
+		SupportFW:    false,
+		LaunchEmb:    "SystemAO/file_system/zip_extractor.html",
+		SupportEmb:   true,
+		InitEmbSize:  []int{260, 120},
+		SupportedExt: []string{".zip"},
+	})
+
+	//Create user root if not exists
+	err := os.MkdirAll(*root_directory+"users/", 0755)
+	if err != nil {
+		log.Println("Failed to create system storage root.")
+		panic(err)
+	}
+
+	//Create database table if not exists
+	err = sysdb.NewTable("fs")
+	if err != nil {
+		log.Println("Failed to create table for file system")
+		panic(err)
+	}
+
+	//Create a RenderHandler for caching thumbnails
+	thumbRenderHandler = metadata.NewRenderHandler()
+
+	/*
+		Share Related Registering
+
+		This section of functions create and register the file share service
+		for the arozos
+
+	*/
+	//Create a share manager to handle user file sharae
+	shareManager = share.NewShareManager(share.Options{
+		AuthAgent:   authAgent,
+		Database:    sysdb,
+		UserHandler: userHandler,
+		HostName:    *host_name,
+		TmpFolder:   *tmp_directory,
+	})
+
+	//Share related functions
+	router.HandleFunc("/system/file_system/share/new", shareManager.HandleCreateNewShare)
+	router.HandleFunc("/system/file_system/share/delete", shareManager.HandleDeleteShare)
+	router.HandleFunc("/system/file_system/share/edit", shareManager.HandleEditShare)
+
+	//Handle the main share function
+	http.HandleFunc("/share", shareManager.HandleShareAccess)
+
+	/*
+		Nighly Tasks
+
+		These functions allow file system to clear and maintain
+		the arozos file system when no one is using the system
+	*/
+
+	//Clear tmp folder if files is placed here too long
+	RegisterNightlyTask(system_fs_clearOldTmpFiles)
+
+	//Clear shares that its parent file no longer exists in the system
+	shareManager.ValidateAndClearShares()
+	RegisterNightlyTask(shareManager.ValidateAndClearShares)
+
+}
+
+/*
+	File Search
+
+	Handle file search in wildcard and recursive search
+
+*/
+
+func system_fs_handleFileSearch(w http.ResponseWriter, r *http.Request) {
+	//Get the user information
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	//Get the search target root path
+	vpath, err := mv(r, "path", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid vpath given")
+		return
+	}
+
+	keyword, err := mv(r, "keyword", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid keyword given")
+		return
+	}
+
+	//Translate the vpath to realpath
+	rpath, err := userinfo.VirtualPathToRealPath(vpath)
+	if err != nil {
+		sendErrorResponse(w, "Invalid path given")
+		return
+	}
+
+	//Check if the search mode is recursive keyword or wildcard
+	if len(keyword) > 1 && keyword[:1] == "/" {
+		//Wildcard
+		wildcard := keyword[1:]
+		matchingFiles, err := filepath.Glob(filepath.Join(rpath, wildcard))
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+
+		//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, _ := userinfo.RealPathToVirtualPath(matchedFile)
+			results = append(results, fs.GetFileDataFromPath(thisVpath, matchedFile, 2))
+
+		}
+
+		if escaped {
+			sendErrorResponse(w, "Search keywords contain escape character!")
+			return
+		}
+
+		//OK. Tidy up the results
+		js, _ := json.Marshal(results)
+		sendJSONResponse(w, string(js))
+	} else {
+		//Recursive keyword
+		results := []fs.FileData{}
+		err := filepath.Walk(rpath, func(path string, info os.FileInfo, err error) error {
+
+			if strings.Contains(filepath.Base(path), keyword) {
+				//This is a matching file
+				if !fs.IsInsideHiddenFolder(path) {
+					thisVpath, _ := userinfo.RealPathToVirtualPath(path)
+					results = append(results, fs.GetFileDataFromPath(thisVpath, path, 2))
+				}
+
+			}
+			return nil
+		})
+
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+
+		//OK. Tidy up the results
+		js, _ := json.Marshal(results)
+		sendJSONResponse(w, string(js))
+	}
+
+}
+
+/*
+	Handle low-memory upload operations
+
+	This function is specailly designed to work with low memory devices
+	(e.g. ZeroPi / Orange Pi Zero with 512MB RAM)
+*/
+func system_fs_handleLowMemoryUpload(w http.ResponseWriter, r *http.Request) {
+	//Get user info
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		w.WriteHeader(http.StatusUnauthorized)
+		w.Write([]byte("401 - Unauthorized"))
+		return
+	}
+
+	//Get filename and upload path
+	filename, err := mv(r, "filename", false)
+	if filename == "" || err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Invalid filename given"))
+		return
+	}
+
+	//Get upload target directory
+	uploadTarget, err := mv(r, "path", false)
+	if uploadTarget == "" || err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Invalid path given"))
+		return
+	}
+
+	//Check if the user can write to this folder
+	if !userinfo.CanWrite(uploadTarget) {
+		//No permission
+		w.WriteHeader(http.StatusForbidden)
+		w.Write([]byte("403 - Access Denied"))
+		return
+	}
+
+	//Translate the upload target directory
+	realUploadPath, err := userinfo.VirtualPathToRealPath(uploadTarget)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Path translation failed"))
+		return
+	}
+
+	//Generate an UUID for this upload
+	uploadUUID := uuid.NewV4().String()
+	uploadFolder := filepath.Join(*tmp_directory, "uploads", uploadUUID)
+	os.MkdirAll(uploadFolder, 0700)
+	targetUploadLocation := filepath.Join(realUploadPath, filename)
+	if !fileExists(realUploadPath) {
+		os.MkdirAll(realUploadPath, 0755)
+	}
+
+	//Start websocket connection
+	var upgrader = websocket.Upgrader{}
+	c, err := upgrader.Upgrade(w, r, nil)
+	defer c.Close()
+
+	//Handle WebSocket upload
+	blockCounter := 0
+	chunkName := []string{}
+	lastChunkArrivalTime := time.Now().Unix()
+
+	//Setup a timeout listener, check if connection still active every 1 minute
+	ticker := time.NewTicker(60 * time.Second)
+	done := make(chan bool)
+	go func() {
+		for {
+			select {
+			case <-done:
+				return
+			case <-ticker.C:
+				if time.Now().Unix()-lastChunkArrivalTime > 300 {
+					//Already 5 minutes without new data arraival. Stop connection
+					log.Println("Upload WebSocket connection timeout. Disconnecting.")
+					c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
+					time.Sleep(1 * time.Second)
+					c.Close()
+					return
+				}
+			}
+		}
+	}()
+
+	totalFileSize := int64(0)
+	for {
+		mt, message, err := c.ReadMessage()
+		if err != nil {
+			//Connection closed by client. Clear the tmp folder and exit
+			log.Println("Upload terminated by client. Cleaning tmp folder.")
+			//Clear the tmp folder
+			time.Sleep(1 * time.Second)
+			os.RemoveAll(uploadFolder)
+			return
+		}
+		//The mt should be 2 = binary for file upload and 1 for control syntax
+		if mt == 1 {
+			msg := strings.TrimSpace(string(message))
+			if msg == "done" {
+				//Start the merging process
+				log.Println(userinfo.Username + " uploaded a file: " + filepath.Base(targetUploadLocation))
+				break
+			} else {
+				//Unknown operations
+
+			}
+		} else if mt == 2 {
+			//File block. Save it to tmp folder
+			chunkFilepath := filepath.Join(uploadFolder, "upld_"+strconv.Itoa(blockCounter))
+			chunkName = append(chunkName, chunkFilepath)
+			ioutil.WriteFile(chunkFilepath, message, 0700)
+
+			//Update the last upload chunk time
+			lastChunkArrivalTime = time.Now().Unix()
+
+			//Check if the file size is too big
+			totalFileSize += fs.GetFileSize(chunkFilepath)
+			if totalFileSize > max_upload_size {
+				//File too big
+				c.WriteMessage(1, []byte(`{\"error\":\"File size too large.\"}`))
+
+				//Close the connection
+				c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
+				time.Sleep(1 * time.Second)
+				c.Close()
+
+				//Clear the tmp files
+				os.RemoveAll(uploadFolder)
+				return
+			}
+			blockCounter++
+
+			//Request client to send the next chunk
+			c.WriteMessage(1, []byte("next"))
+
+		}
+		//log.Println("recv:", len(message), "type", mt)
+	}
+
+	//Merge the file
+
+	out, err := os.OpenFile(targetUploadLocation, os.O_CREATE|os.O_WRONLY, 0755)
+	if err != nil {
+		log.Println("Failed to open file:", err)
+		c.WriteMessage(1, []byte(`{\"error\":\"Failed to open destination file\"}`))
+		c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
+		time.Sleep(1 * time.Second)
+		c.Close()
+		return
+	}
+	defer out.Close()
+
+	for _, filesrc := range chunkName {
+		srcChunkReader, err := os.Open(filesrc)
+		if err != nil {
+			log.Println("Failed to open Source Chunk", filesrc, " with error ", err.Error())
+			c.WriteMessage(1, []byte(`{\"error\":\"Failed to open Source Chunk\"}`))
+			return
+		}
+		io.Copy(out, srcChunkReader)
+		srcChunkReader.Close()
+	}
+
+	//Set owner of the new uploaded file
+	userinfo.SetOwnerOfFile(targetUploadLocation)
+
+	//Return complete signal
+	c.WriteMessage(1, []byte("OK"))
+
+	//Stop the timeout listner
+	done <- true
+
+	//Clear the tmp folder
+	time.Sleep(1 * time.Second)
+	err = os.RemoveAll(uploadFolder)
+	if err != nil {
+		log.Println(err)
+	}
+
+	//Close WebSocket connection after finished
+	c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
+	time.Sleep(1 * time.Second)
+	c.Close()
+
+}
+
+/*
+	Handle FORM POST based upload
+
+	This function is design for general SBCs or computers with more than 1GB of RAM
+	(e.g. Raspberry Pi 4 / Linux Server)
+*/
+func system_fs_handleUpload(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	username := userinfo.Username
+
+	//Limit the max upload size to the user defined size
+	if max_upload_size != 0 {
+		r.Body = http.MaxBytesReader(w, r.Body, max_upload_size)
+	}
+
+	//Check if this is running under demo mode. If yes, reject upload
+	if *demo_mode {
+		sendErrorResponse(w, "You cannot upload in demo mode")
+		return
+	}
+
+	err = r.ParseMultipartForm(int64(*upload_buf) << 20)
+	if err != nil {
+		//Filesize too big
+		log.Println(err)
+		sendErrorResponse(w, "File too large")
+		return
+	}
+
+	file, handler, err := r.FormFile("file")
+	if err != nil {
+		log.Println("Error Retrieving File from upload by user: " + username)
+		sendErrorResponse(w, "Unable to parse file from upload")
+		return
+	}
+
+	//Get upload target directory
+	uploadTarget, _ := mv(r, "path", true)
+	if uploadTarget == "" {
+		sendErrorResponse(w, "Upload target cannot be empty.")
+		return
+	}
+
+	//Translate the upload target directory
+	realUploadPath, err := userinfo.VirtualPathToRealPath(uploadTarget)
+
+	if err != nil {
+		sendErrorResponse(w, "Upload target is invalid or permission denied.")
+		return
+	}
+
+	storeFilename := handler.Filename //Filename of the uploaded file
+	destFilepath := filepath.ToSlash(filepath.Clean(realUploadPath)) + "/" + storeFilename
+
+	if !fileExists(filepath.Dir(destFilepath)) {
+		os.MkdirAll(filepath.Dir(destFilepath), 0755)
+	}
+
+	//Check if the upload target is read only.
+	accmode := userinfo.GetPathAccessPermission(uploadTarget)
+	if accmode == "readonly" {
+		sendErrorResponse(w, "The upload target is Read Only.")
+		return
+	} else if accmode == "denied" {
+		sendErrorResponse(w, "Access Denied")
+		return
+	}
+
+	//Check for storage quota
+	uploadFileSize := handler.Size
+	if !userinfo.StorageQuota.HaveSpace(uploadFileSize) {
+		sendErrorResponse(w, "Storage Quota Full")
+		return
+	}
+
+	//Prepare the file to be created (uploaded)
+	destination, err := os.Create(destFilepath)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	defer destination.Close()
+	defer file.Close()
+
+	//Move the file to destination file location
+	if *enable_asyncFileUpload {
+		//Use Async upload method
+		go func(r *http.Request, file multipart.File, destination *os.File, userinfo *user.User) {
+			//Do the file copying using a buffered reader
+			buf := make([]byte, *file_opr_buff)
+			for {
+				n, err := file.Read(buf)
+				if err != nil && err != io.EOF {
+					log.Println(err.Error())
+					return
+				}
+				if n == 0 {
+					break
+				}
+
+				if _, err := destination.Write(buf[:n]); err != nil {
+					log.Println(err.Error())
+					return
+				}
+			}
+
+			//Clear up buffered files
+			r.MultipartForm.RemoveAll()
+
+			//Set the ownership of file
+			userinfo.SetOwnerOfFile(destFilepath)
+
+			//Perform a GC afterward
+			runtime.GC()
+
+		}(r, file, destination, userinfo)
+	} else {
+		//Use blocking upload and move method
+		buf := make([]byte, *file_opr_buff)
+		for {
+			n, err := file.Read(buf)
+			if err != nil && err != io.EOF {
+				log.Println(err.Error())
+				return
+			}
+			if n == 0 {
+				break
+			}
+
+			if _, err := destination.Write(buf[:n]); err != nil {
+				log.Println(err.Error())
+				return
+			}
+		}
+
+		//Clear up buffered files
+		r.MultipartForm.RemoveAll()
+
+		//Set the ownership of file
+		userinfo.SetOwnerOfFile(destFilepath)
+	}
+
+	//Finish up the upload
+
+	//fmt.Printf("Uploaded File: %+v\n", handler.Filename)
+	//fmt.Printf("File Size: %+v\n", handler.Size)
+	//fmt.Printf("MIME Header: %+v\n", handler.Header)
+	//fmt.Println("Upload target: " + realUploadPath)
+
+	//Fnish upload. Fix the tmp filename
+	log.Println(username + " uploaded a file: " + handler.Filename)
+
+	//Do upload finishing stuff
+	//Perform a GC
+	runtime.GC()
+
+	//Completed
+	sendOK(w)
+
+	return
+}
+
+//Validate if the copy and target process will involve file overwriting problem.
+func system_fs_validateFileOpr(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	vsrcFiles, _ := mv(r, "src", true)
+	vdestFile, _ := mv(r, "dest", true)
+	var duplicateFiles []string
+
+	//Loop through all files are see if there are duplication during copy and paste
+	sourceFiles := []string{}
+	decodedSourceFiles, _ := url.QueryUnescape(vsrcFiles)
+	err = json.Unmarshal([]byte(decodedSourceFiles), &sourceFiles)
+	if err != nil {
+		sendErrorResponse(w, "Source file JSON parse error.")
+		return
+	}
+
+	rdestFile, _ := userinfo.VirtualPathToRealPath(vdestFile)
+	for _, file := range sourceFiles {
+		rsrcFile, _ := userinfo.VirtualPathToRealPath(string(file))
+		if fileExists(rdestFile + filepath.Base(rsrcFile)) {
+			//File exists already.
+			vpath, _ := userinfo.RealPathToVirtualPath(rsrcFile)
+			duplicateFiles = append(duplicateFiles, vpath)
+		}
+
+	}
+
+	jsonString, _ := json.Marshal(duplicateFiles)
+	sendJSONResponse(w, string(jsonString))
+	return
+}
+
+//Scan all directory and get trash file and send back results with WebSocket
+func system_fs_WebSocketScanTrashBin(w http.ResponseWriter, r *http.Request) {
+	//Get and check user permission
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	//Upgrade to websocket
+	var upgrader = websocket.Upgrader{}
+	c, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - " + err.Error()))
+		log.Print("Websocket Upgrade Error:", err.Error())
+		return
+	}
+
+	//Start Scanning
+	scanningRoots := []string{}
+	//Get all roots to scan
+	for _, storage := range userinfo.GetAllFileSystemHandler() {
+		storageRoot := storage.Path
+		scanningRoots = append(scanningRoots, storageRoot)
+	}
+
+	for _, rootPath := range scanningRoots {
+		err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
+			oneLevelUpper := filepath.Base(filepath.Dir(path))
+			if oneLevelUpper == ".trash" {
+				//This is a trashbin dir.
+				file := path
+
+				//Parse the trashFile struct
+				timestamp := filepath.Ext(file)[1:]
+				originalName := strings.TrimSuffix(filepath.Base(file), filepath.Ext(filepath.Base(file)))
+				originalExt := filepath.Ext(filepath.Base(originalName))
+				virtualFilepath, _ := userinfo.RealPathToVirtualPath(file)
+				virtualOrgPath, _ := userinfo.RealPathToVirtualPath(filepath.Dir(filepath.Dir(file)))
+				rawsize := fs.GetFileSize(file)
+				timestampInt64, _ := StringToInt64(timestamp)
+				removeTimeDate := time.Unix(timestampInt64, 0)
+				if IsDir(file) {
+					originalExt = ""
+				}
+
+				thisTrashFileObject := trashedFile{
+					Filename:         filepath.Base(file),
+					Filepath:         virtualFilepath,
+					FileExt:          originalExt,
+					IsDir:            IsDir(file),
+					Filesize:         int64(rawsize),
+					RemoveTimestamp:  timestampInt64,
+					RemoveDate:       timeToString(removeTimeDate),
+					OriginalPath:     virtualOrgPath,
+					OriginalFilename: originalName,
+				}
+
+				//Send out the result as JSON string
+				js, _ := json.Marshal(thisTrashFileObject)
+				err := c.WriteMessage(1, js)
+				if err != nil {
+					//Connection already closed
+					return err
+				}
+			}
+
+			return nil
+		})
+
+		if err != nil {
+			//Scan or client connection error (Connection closed?)
+			return
+		}
+	}
+
+	//Close connection after finished
+	c.Close()
+
+}
+
+//Scan all the directory and get trash files within the system
+func system_fs_scanTrashBin(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	username := userinfo.Username
+
+	results := []trashedFile{}
+	files, err := system_fs_listTrash(username)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	//Get information of each files and process it into results
+	for _, file := range files {
+		timestamp := filepath.Ext(file)[1:]
+		originalName := strings.TrimSuffix(filepath.Base(file), filepath.Ext(filepath.Base(file)))
+		originalExt := filepath.Ext(filepath.Base(originalName))
+		virtualFilepath, _ := userinfo.RealPathToVirtualPath(file)
+		virtualOrgPath, _ := userinfo.RealPathToVirtualPath(filepath.Dir(filepath.Dir(file)))
+		rawsize := fs.GetFileSize(file)
+		timestampInt64, _ := StringToInt64(timestamp)
+		removeTimeDate := time.Unix(timestampInt64, 0)
+		if IsDir(file) {
+			originalExt = ""
+		}
+		results = append(results, trashedFile{
+			Filename:         filepath.Base(file),
+			Filepath:         virtualFilepath,
+			FileExt:          originalExt,
+			IsDir:            IsDir(file),
+			Filesize:         int64(rawsize),
+			RemoveTimestamp:  timestampInt64,
+			RemoveDate:       timeToString(removeTimeDate),
+			OriginalPath:     virtualOrgPath,
+			OriginalFilename: originalName,
+		})
+	}
+
+	//Sort the results by date, latest on top
+	sort.Slice(results[:], func(i, j int) bool {
+		return results[i].RemoveTimestamp > results[j].RemoveTimestamp
+	})
+
+	//Format and return the json results
+	jsonString, _ := json.Marshal(results)
+	sendJSONResponse(w, string(jsonString))
+}
+
+//Restore a trashed file to its parent dir
+func system_fs_restoreFile(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	targetTrashedFile, err := mv(r, "src", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid src given")
+		return
+	}
+
+	//Translate it to realpath
+	realpath, _ := userinfo.VirtualPathToRealPath(targetTrashedFile)
+	if !fileExists(realpath) {
+		sendErrorResponse(w, "File not exists")
+		return
+	}
+
+	//Check if this is really a trashed file
+	if filepath.Base(filepath.Dir(realpath)) != ".trash" {
+		sendErrorResponse(w, "File not in trashbin")
+		return
+	}
+
+	//OK to proceed.
+	targetPath := filepath.ToSlash(filepath.Dir(filepath.Dir(realpath))) + "/" + strings.TrimSuffix(filepath.Base(realpath), filepath.Ext(filepath.Base(realpath)))
+	//log.Println(targetPath);
+	os.Rename(realpath, targetPath)
+
+	//Check if the parent dir has no more fileds. If yes, remove it
+	filescounter, _ := filepath.Glob(filepath.Dir(realpath) + "/*")
+	if len(filescounter) == 0 {
+		os.Remove(filepath.Dir(realpath))
+	}
+
+	sendOK(w)
+}
+
+//Clear all trashed file in the system
+func system_fs_clearTrashBin(w http.ResponseWriter, r *http.Request) {
+	u, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	username := u.Username
+
+	fileList, err := system_fs_listTrash(username)
+	if err != nil {
+		sendErrorResponse(w, "Unable to clear trash: "+err.Error())
+		return
+	}
+
+	//Get list success. Remove each of them.
+	for _, file := range fileList {
+		isOwner := u.IsOwnerOfFile(file)
+		if isOwner {
+			//This user own this system. Remove this file from his quota
+			u.RemoveOwnershipFromFile(file)
+		}
+
+		os.RemoveAll(file)
+		//Check if its parent directory have no files. If yes, remove the dir itself as well.
+		filesInThisTrashBin, _ := filepath.Glob(filepath.Dir(file) + "/*")
+		if len(filesInThisTrashBin) == 0 {
+			os.Remove(filepath.Dir(file))
+		}
+	}
+
+	sendOK(w)
+}
+
+//Get all trash in a string list
+func system_fs_listTrash(username string) ([]string, error) {
+	userinfo, _ := userHandler.GetUserInfoFromUsername(username)
+	scanningRoots := []string{}
+	//Get all roots to scan
+	for _, storage := range userinfo.GetAllFileSystemHandler() {
+		storageRoot := storage.Path
+		scanningRoots = append(scanningRoots, storageRoot)
+	}
+
+	files := []string{}
+	for _, rootPath := range scanningRoots {
+		err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error {
+			oneLevelUpper := filepath.Base(filepath.Dir(path))
+			if oneLevelUpper == ".trash" {
+				//This is a trashbin dir.
+				files = append(files, path)
+			}
+			return nil
+		})
+		if err != nil {
+			return []string{}, errors.New("Failed to scan file system.")
+		}
+	}
+
+	return files, nil
+}
+
+/*
+	Handle new file or folder functions
+
+	Required information
+	@type {folder / file}
+	@ext {any that is listed in the template folder}
+	if no paramter is passed in, default listing all the supported template file
+*/
+
+func system_fs_handleNewObjects(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	fileType, _ := mv(r, "type", true)     //File creation type, {file, folder}
+	vsrc, _ := mv(r, "src", true)          //Virtual file source folder, do not include filename
+	filename, _ := mv(r, "filename", true) //Filename for the new file
+
+	if fileType == "" && filename == "" {
+		//List all the supported new filetype
+		if !fileExists("system/newitem/") {
+			os.MkdirAll("system/newitem/", 0755)
+		}
+
+		type newItemObject struct {
+			Desc string
+			Ext  string
+		}
+
+		var newItemList []newItemObject
+		newItemTemplate, _ := filepath.Glob("system/newitem/*")
+		for _, file := range newItemTemplate {
+			thisItem := new(newItemObject)
+			thisItem.Desc = strings.TrimSuffix(filepath.Base(file), filepath.Ext(file))
+			thisItem.Ext = filepath.Ext(file)[1:]
+			newItemList = append(newItemList, *thisItem)
+		}
+
+		jsonString, err := json.Marshal(newItemList)
+		if err != nil {
+			log.Println("*File System* Unable to parse JSON string for new item list!")
+			sendErrorResponse(w, "Unable to parse new item list. See server log for more information.")
+			return
+		}
+		sendJSONResponse(w, string(jsonString))
+		return
+	} else if fileType != "" && filename != "" {
+		if vsrc == "" {
+			sendErrorResponse(w, "Missing paramter: 'src'")
+			return
+		}
+		//Translate the path to realpath
+		rpath, err := userinfo.VirtualPathToRealPath(vsrc)
+		if err != nil {
+			sendErrorResponse(w, "Invalid path given.")
+			return
+		}
+
+		//Check if directory is readonly
+		accmode := userinfo.GetPathAccessPermission(vsrc)
+		if accmode == "readonly" {
+			sendErrorResponse(w, "This directory is Read Only.")
+			return
+		} else if accmode == "denied" {
+			sendErrorResponse(w, "Access Denied")
+			return
+		}
+		//Check if the file already exists. If yes, fix its filename.
+		newfilePath := rpath + filename
+
+		if fileType == "file" {
+			for fileExists(newfilePath) {
+				sendErrorResponse(w, "Given filename already exists.")
+				return
+			}
+			ext := filepath.Ext(filename)
+
+			if ext == "" {
+				//This is a file with no extension.
+				f, err := os.Create(newfilePath)
+				if err != nil {
+					log.Println("*File System* " + err.Error())
+					sendErrorResponse(w, err.Error())
+					return
+				}
+				f.Close()
+			} else {
+				templateFile, _ := filepath.Glob("system/newitem/*" + ext)
+				if len(templateFile) == 0 {
+					//This file extension is not in template
+					f, err := os.Create(newfilePath)
+					if err != nil {
+						log.Println("*File System* " + err.Error())
+						sendErrorResponse(w, err.Error())
+						return
+					}
+					f.Close()
+				} else {
+					//Copy file from templateFile[0] to current dir with the given name
+					input, _ := ioutil.ReadFile(templateFile[0])
+					err := ioutil.WriteFile(newfilePath, input, 0755)
+					if err != nil {
+						log.Println("*File System* " + err.Error())
+						sendErrorResponse(w, err.Error())
+						return
+					}
+				}
+			}
+
+		} else if fileType == "folder" {
+			if fileExists(newfilePath) {
+				sendErrorResponse(w, "Given folder already exists.")
+				return
+			}
+			//Create the folder at target location
+			err := os.Mkdir(newfilePath, 0755)
+			if err != nil {
+				sendErrorResponse(w, err.Error())
+				return
+			}
+		}
+
+		sendJSONResponse(w, "\"OK\"")
+	} else {
+		sendErrorResponse(w, "Missing paramter(s).")
+		return
+	}
+}
+
+/*
+
+	Handle file operations via WebSocket
+
+	This handler only handle zip, unzip, copy and move. Not other operations.
+	For other operations, please use the legacy handleOpr endpoint
+*/
+
+func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
+	//Get and check user permission
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	operation, _ := mv(r, "opr", false) //Accept copy and move
+	vsrcFiles, _ := mv(r, "src", false)
+	vdestFile, _ := mv(r, "dest", false)
+	existsOpr, _ := mv(r, "existsresp", false)
+
+	if existsOpr == "" {
+		existsOpr = "keep"
+	}
+
+	//Decode the source file list
+	var sourceFiles []string
+	tmp := []string{}
+	decodedSourceFiles, _ := url.QueryUnescape(vsrcFiles)
+	err = json.Unmarshal([]byte(decodedSourceFiles), &sourceFiles)
+	if err != nil {
+		log.Println("Source file JSON parse error.", err.Error())
+		sendErrorResponse(w, "Source file JSON parse error.")
+		return
+	}
+
+	//Bugged char filtering
+	for _, src := range sourceFiles {
+		tmp = append(tmp, strings.ReplaceAll(src, "{{plug_sign}}", "+"))
+	}
+	sourceFiles = tmp
+
+	vdestFile = strings.ReplaceAll(vdestFile, "{{plug_sign}}", "+")
+
+	//Decode the target position
+	escapedVdest, _ := url.QueryUnescape(vdestFile)
+	vdestFile = escapedVdest
+	rdestFile, _ := userinfo.VirtualPathToRealPath(vdestFile)
+
+	//Permission checking
+	if !userinfo.CanWrite(vdestFile) {
+		log.Println(vdestFile)
+		w.WriteHeader(http.StatusForbidden)
+		w.Write([]byte("403 - Access Denied"))
+		return
+	}
+
+	//Check if opr is suported
+	if operation == "move" || operation == "copy" || operation == "zip" || operation == "unzip" {
+
+	} else {
+		log.Println("This file operation is not supported on WebSocket file operations endpoint. Please use the legacy endpoint instead. Received: ", operation)
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - Not supported operation"))
+		return
+	}
+
+	//Upgrade to websocket
+	var upgrader = websocket.Upgrader{}
+	c, err := upgrader.Upgrade(w, r, nil)
+	if err != nil {
+		w.WriteHeader(http.StatusInternalServerError)
+		w.Write([]byte("500 - " + err.Error()))
+		log.Print("Websocket Upgrade Error:", err.Error())
+		return
+	}
+
+	type ProgressUpdate struct {
+		LatestFile string
+		Progress   int
+		Error      string
+	}
+
+	if operation == "zip" {
+		//Zip files
+		outputFilename := filepath.Join(rdestFile, filepath.Base(rdestFile)) + ".zip"
+		if len(sourceFiles) == 1 {
+			//Use the basename of the source file as zip file name
+			outputFilename = filepath.Join(rdestFile, filepath.Base(sourceFiles[0])) + ".zip"
+		}
+
+		//Translate source Files into real paths
+		realSourceFiles := []string{}
+		for _, vsrcs := range sourceFiles {
+			rsrc, err := userinfo.VirtualPathToRealPath(vsrcs)
+			if err != nil {
+				stopStatus := ProgressUpdate{
+					LatestFile: filepath.Base(rsrc),
+					Progress:   -1,
+					Error:      "File not exists",
+				}
+				js, _ := json.Marshal(stopStatus)
+				c.WriteMessage(1, js)
+				c.Close()
+			}
+
+			realSourceFiles = append(realSourceFiles, rsrc)
+		}
+
+		//Create the zip file
+		fs.ArozZipFileWithProgress(realSourceFiles, outputFilename, false, func(currentFilename string, _ int, _ int, progress float64) {
+			currentStatus := ProgressUpdate{
+				LatestFile: currentFilename,
+				Progress:   int(math.Ceil(progress)),
+				Error:      "",
+			}
+
+			js, _ := json.Marshal(currentStatus)
+			c.WriteMessage(1, js)
+		})
+	} else if operation == "unzip" {
+		//Check if the target destination exists and writable
+		if !userinfo.CanWrite(vdestFile) {
+			stopStatus := ProgressUpdate{
+				LatestFile: filepath.Base(vdestFile),
+				Progress:   -1,
+				Error:      "Access Denied: No Write Permission",
+			}
+			js, _ := json.Marshal(stopStatus)
+			c.WriteMessage(1, js)
+			c.Close()
+		}
+
+		//Create the destination folder
+		os.MkdirAll(rdestFile, 0755)
+
+		//Convert the src files into realpaths
+		realSourceFiles := []string{}
+		for _, vsrcs := range sourceFiles {
+			rsrc, err := userinfo.VirtualPathToRealPath(vsrcs)
+			if err != nil {
+				stopStatus := ProgressUpdate{
+					LatestFile: filepath.Base(rsrc),
+					Progress:   -1,
+					Error:      "File not exists",
+				}
+				js, _ := json.Marshal(stopStatus)
+				c.WriteMessage(1, js)
+				c.Close()
+			}
+
+			realSourceFiles = append(realSourceFiles, rsrc)
+		}
+
+		//Unzip the files
+		fs.ArozUnzipFileWithProgress(realSourceFiles, rdestFile, func(currentFile string, filecount int, totalfile int, progress float64) {
+			//Generate the status update struct
+
+			currentStatus := ProgressUpdate{
+				LatestFile: filepath.Base(currentFile),
+				Progress:   int(math.Ceil(progress)),
+				Error:      "",
+			}
+
+			js, _ := json.Marshal(currentStatus)
+			c.WriteMessage(1, js)
+		})
+
+	} else {
+		//Other operations that allow multiple source files to handle one by one
+		for i := 0; i < len(sourceFiles); i++ {
+			vsrcFile := sourceFiles[i]
+			rsrcFile, _ := userinfo.VirtualPathToRealPath(vsrcFile)
+			//c.WriteMessage(1, message)
+			if !fileExists(rsrcFile) {
+				//This source file not exists. Report Error and Stop
+				stopStatus := ProgressUpdate{
+					LatestFile: filepath.Base(rsrcFile),
+					Progress:   -1,
+					Error:      "File not exists",
+				}
+				js, _ := json.Marshal(stopStatus)
+				c.WriteMessage(1, js)
+				c.Close()
+				return
+			}
+
+			if operation == "move" {
+				fs.FileMove(rsrcFile, rdestFile, existsOpr, false, func(progress int, currentFile string) {
+					//Multply child progress to parent progress
+					blockRatio := float64(100) / float64(len(sourceFiles))
+					overallRatio := blockRatio*float64(i) + blockRatio*(float64(progress)/float64(100))
+
+					//Construct return struct
+					currentStatus := ProgressUpdate{
+						LatestFile: filepath.Base(currentFile),
+						Progress:   int(overallRatio),
+						Error:      "",
+					}
+
+					js, _ := json.Marshal(currentStatus)
+					c.WriteMessage(1, js)
+				})
+			} else if operation == "copy" {
+				fs.FileCopy(rsrcFile, rdestFile, existsOpr, func(progress int, currentFile string) {
+					//Multply child progress to parent progress
+					blockRatio := float64(100) / float64(len(sourceFiles))
+					overallRatio := blockRatio*float64(i) + blockRatio*(float64(progress)/float64(100))
+
+					//Construct return struct
+					currentStatus := ProgressUpdate{
+						LatestFile: filepath.Base(currentFile),
+						Progress:   int(overallRatio),
+						Error:      "",
+					}
+
+					js, _ := json.Marshal(currentStatus)
+					c.WriteMessage(1, js)
+				})
+			}
+		}
+	}
+
+	//Close WebSocket connection after finished
+	time.Sleep(1 * time.Second)
+	c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
+	c.Close()
+
+}
+
+/*
+	Handle file operations
+
+	Support {move, copy, delete, recycle, rename}
+*/
+//Handle file operations.
+func system_fs_handleOpr(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	operation, _ := mv(r, "opr", true)
+	vsrcFiles, _ := mv(r, "src", true)
+	vdestFile, _ := mv(r, "dest", true)
+	vnfilenames, _ := mv(r, "new", true) //Only use when rename or create new file / folder
+
+	//Check if operation valid.
+	if operation == "" {
+		//Undefined operations.
+		sendErrorResponse(w, "Undefined operations paramter: Missing 'opr' in request header.")
+		return
+	}
+
+	//As the user can pass in multiple source files at the same time, parse sourceFiles from json string
+	var sourceFiles []string
+	//This line is required in order to allow passing of special charaters
+	decodedSourceFiles := system_fs_specialURIDecode(vsrcFiles)
+	err = json.Unmarshal([]byte(decodedSourceFiles), &sourceFiles)
+	if err != nil {
+		sendErrorResponse(w, "Source file JSON parse error.")
+		return
+	}
+
+	//Check if new filenames are also valid. If yes, translate it into string array
+	var newFilenames []string
+	if vnfilenames != "" {
+		vnfilenames, _ := url.QueryUnescape(vnfilenames)
+		err = json.Unmarshal([]byte(vnfilenames), &newFilenames)
+		if err != nil {
+			sendErrorResponse(w, "Unable to parse JSON for new filenames")
+			return
+		}
+	}
+
+	if operation == "zip" {
+		//Zip operation. Parse the real filepath list
+		rsrcFiles := []string{}
+		rdestFile, _ := userinfo.VirtualPathToRealPath(vdestFile)
+		for _, vsrcFile := range sourceFiles {
+			rsrcFile, _ := userinfo.VirtualPathToRealPath(string(vsrcFile))
+			if fileExists(rsrcFile) {
+				rsrcFiles = append(rsrcFiles, rsrcFile)
+			}
+		}
+
+		zipFilename := rdestFile
+		if fs.IsDir(rdestFile) {
+			//Append the filename to it
+			if len(rsrcFiles) == 1 {
+				zipFilename = filepath.Join(rdestFile, strings.TrimSuffix(filepath.Base(rsrcFiles[0]), filepath.Ext(filepath.Base(rsrcFiles[0])))+".zip")
+			} else if len(rsrcFiles) > 1 {
+				zipFilename = filepath.Join(rdestFile, filepath.Base(filepath.Dir(rsrcFiles[0]))+".zip")
+			}
+		}
+
+		//Create a zip file at target location
+		err := fs.ArozZipFile(rsrcFiles, zipFilename, false)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+	} else {
+		//For operations that is handled file by file
+		for i, vsrcFile := range sourceFiles {
+			//Convert the virtual path to realpath on disk
+			rsrcFile, _ := userinfo.VirtualPathToRealPath(string(vsrcFile))
+			rdestFile, _ := userinfo.VirtualPathToRealPath(vdestFile)
+			//Check if the source file exists
+			if !fileExists(rsrcFile) {
+				/*
+					Special edge case handler:
+
+					There might be edge case that files are stored in URIEncoded methods
+					e.g. abc def.mp3 --> abc%20cdf.mp3
+
+					In this case, this logic statement should be able to handle this
+				*/
+
+				edgeCaseFilename := filepath.Join(filepath.Dir(rsrcFile), system_fs_specialURIEncode(filepath.Base(rsrcFile)))
+				if fileExists(edgeCaseFilename) {
+					rsrcFile = edgeCaseFilename
+				} else {
+					sendErrorResponse(w, "Source file not exists.")
+					return
+				}
+
+			}
+
+			if operation == "rename" {
+				//Check if the usage is correct.
+				if vdestFile != "" {
+					sendErrorResponse(w, "Rename only accept 'src' and 'new'. Please use move if you want to move a file.")
+					return
+				}
+				//Check if new name paramter is passed in.
+				if len(newFilenames) == 0 {
+					sendErrorResponse(w, "Missing paramter (JSON string): 'new'")
+					return
+				}
+				//Check if the source filenames and new filenanmes match
+				if len(newFilenames) != len(sourceFiles) {
+					sendErrorResponse(w, "New filenames do not match with source filename's length.")
+					return
+				}
+
+				//Check if the target dir is not readonly
+				accmode := userinfo.GetPathAccessPermission(string(vsrcFile))
+				if accmode == "readonly" {
+					sendErrorResponse(w, "This directory is Read Only.")
+					return
+				} else if accmode == "denied" {
+					sendErrorResponse(w, "Access Denied")
+					return
+				}
+
+				thisFilename := newFilenames[i]
+				//Check if the name already exists. If yes, return false
+				if fileExists(filepath.Dir(rsrcFile) + "/" + thisFilename) {
+					sendErrorResponse(w, "File already exists")
+					return
+				}
+
+				//Everything is ok. Rename the file.
+				targetNewName := filepath.Dir(rsrcFile) + "/" + thisFilename
+				err = os.Rename(rsrcFile, targetNewName)
+				if err != nil {
+					sendErrorResponse(w, err.Error())
+					return
+				}
+
+			} else if operation == "move" {
+				//File move operation. Check if the source file / dir and target directory exists
+				/*
+					Example usage from file explorer
+					$.ajax({
+						type: 'POST',
+						url: `/system/file_system/fileOpr`,
+						data: {opr: "move" ,src: JSON.stringify(fileList), dest: targetDir},
+						success: function(data){
+							if (data.error !== undefined){
+								msgbox("remove",data.error);
+							}else{
+								//OK, do something
+							}
+						}
+					});
+				*/
+
+				if !fileExists(rsrcFile) {
+					sendErrorResponse(w, "Source file not exists")
+					return
+				}
+
+				//Check if the source file is read only.
+				accmode := userinfo.GetPathAccessPermission(string(vsrcFile))
+				if accmode == "readonly" {
+					sendErrorResponse(w, "This source file is Read Only.")
+					return
+				} else if accmode == "denied" {
+					sendErrorResponse(w, "Access Denied")
+					return
+				}
+
+				if rdestFile == "" {
+					sendErrorResponse(w, "Undefined dest location.")
+					return
+				}
+
+				//Get exists overwrite mode
+				existsOpr, _ := mv(r, "existsresp", true)
+
+				//Check if use fast move instead
+				//Check if the source and destination folder are under the same root. If yes, use os.Rename for faster move operations
+
+				underSameRoot := false
+				//Check if the two files are under the same user root path
+
+				srcAbs, _ := filepath.Abs(rsrcFile)
+				destAbs, _ := filepath.Abs(rdestFile)
+
+				//Check other storage path and see if they are under the same root
+				for _, rootPath := range userinfo.GetAllFileSystemHandler() {
+					thisRoot := rootPath.Path
+					thisRootAbs, err := filepath.Abs(thisRoot)
+					if err != nil {
+						continue
+					}
+					if strings.Contains(srcAbs, thisRootAbs) && strings.Contains(destAbs, thisRootAbs) {
+						underSameRoot = true
+					}
+				}
+
+				//Updates 19-10-2020: Added ownership management to file move and copy
+				userinfo.RemoveOwnershipFromFile(rsrcFile)
+
+				err = fs.FileMove(rsrcFile, rdestFile, existsOpr, underSameRoot, nil)
+				if err != nil {
+					sendErrorResponse(w, err.Error())
+					//Restore the ownership if remove failed
+					userinfo.SetOwnerOfFile(rsrcFile)
+					return
+				}
+
+				//Set user to own the new file
+				userinfo.SetOwnerOfFile(filepath.ToSlash(filepath.Clean(rdestFile)) + "/" + filepath.Base(rsrcFile))
+
+			} else if operation == "copy" {
+				//Copy file. See move example and change 'opr' to 'copy'
+				if !fileExists(rsrcFile) {
+					sendErrorResponse(w, "Source file not exists")
+					return
+				}
+
+				//Check if the desintation is read only.
+				if !userinfo.CanWrite(vdestFile) {
+					sendErrorResponse(w, "Access Denied.")
+					return
+				}
+
+				if !fileExists(rdestFile) {
+					if fileExists(filepath.Dir(rdestFile)) {
+						//User pass in the whole path for the folder. Report error usecase.
+						sendErrorResponse(w, "Dest location should be an existing folder instead of the full path of the copied file.")
+						return
+					}
+					sendErrorResponse(w, "Dest folder not found")
+					return
+				}
+
+				existsOpr, _ := mv(r, "existsresp", true)
+
+				//Check if the user have space for the extra file
+				if !userinfo.StorageQuota.HaveSpace(fs.GetFileSize(rdestFile)) {
+					sendErrorResponse(w, "Storage Quota Full")
+					return
+				}
+
+				err = fs.FileCopy(rsrcFile, rdestFile, existsOpr, nil)
+				if err != nil {
+					sendErrorResponse(w, err.Error())
+					return
+				}
+
+				//Set user to own this file
+				userinfo.SetOwnerOfFile(filepath.ToSlash(filepath.Clean(rdestFile)) + "/" + filepath.Base(rsrcFile))
+
+			} else if operation == "delete" {
+				//Delete the file permanently
+				if !fileExists(rsrcFile) {
+					//Check if it is a non escapted file instead
+					sendErrorResponse(w, "Source file not exists")
+					return
+
+				}
+
+				//Check if the desintation is read only.
+				/*
+					accmode := userinfo.GetPathAccessPermission(string(vsrcFile))
+					if accmode == "readonly" {
+						sendErrorResponse(w, "This directory is Read Only.")
+						return
+					} else if accmode == "denied" {
+						sendErrorResponse(w, "Access Denied")
+						return
+					}
+				*/
+				if !userinfo.CanWrite(vsrcFile) {
+					sendErrorResponse(w, "Access Denied.")
+					return
+				}
+
+				//Check if the user own this file
+				isOwner := userinfo.IsOwnerOfFile(rsrcFile)
+				if isOwner {
+					//This user own this system. Remove this file from his quota
+					userinfo.RemoveOwnershipFromFile(rsrcFile)
+				}
+
+				//Check if this file has any cached files. If yes, remove it
+				if fileExists(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/" + filepath.Base(rsrcFile) + ".jpg") {
+					os.Remove(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/" + filepath.Base(rsrcFile) + ".jpg")
+				}
+
+				//Clear the cache folder if there is no files inside
+				fc, _ := filepath.Glob(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/*")
+				if len(fc) == 0 {
+					os.Remove(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/")
+				}
+
+				os.RemoveAll(rsrcFile)
+
+			} else if operation == "recycle" {
+				//Put it into a subfolder named trash and allow it to to be removed later
+				if !fileExists(rsrcFile) {
+					//Check if it is a non escapted file instead
+					sendErrorResponse(w, "Source file not exists")
+					return
+
+				}
+
+				//Check if the upload target is read only.
+				//Updates 20 Jan 2021: Replace with CanWrite handler
+				/*
+					accmode := userinfo.GetPathAccessPermission(string(vsrcFile))
+					if accmode == "readonly" {
+						sendErrorResponse(w, "This directory is Read Only.")
+						return
+					} else if accmode == "denied" {
+						sendErrorResponse(w, "Access Denied")
+						return
+					}*/
+				if !userinfo.CanWrite(vsrcFile) {
+					sendErrorResponse(w, "Access Denied.")
+					return
+				}
+
+				//Check if this file has any cached files. If yes, remove it
+				if fileExists(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/" + filepath.Base(rsrcFile) + ".jpg") {
+					os.Remove(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/" + filepath.Base(rsrcFile) + ".jpg")
+				}
+
+				//Clear the cache folder if there is no files inside
+				fc, _ := filepath.Glob(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/*")
+				if len(fc) == 0 {
+					os.Remove(filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.cache/")
+				}
+
+				//Create a trash directory for this folder
+				trashDir := filepath.ToSlash(filepath.Dir(rsrcFile)) + "/.trash/"
+				os.MkdirAll(trashDir, 0755)
+				hidden.HideFile(trashDir)
+				os.Rename(rsrcFile, trashDir+filepath.Base(rsrcFile)+"."+Int64ToString(GetUnixTime()))
+			} else if operation == "unzip" {
+				//Unzip the file to destination
+
+				//Check if the user can write to the target dest file
+				if userinfo.CanWrite(string(vdestFile)) == false {
+					sendErrorResponse(w, "Access Denied.")
+					return
+				}
+
+				//Make the rdest directory if not exists
+				if !fileExists(rdestFile) {
+					err = os.MkdirAll(rdestFile, 0755)
+					if err != nil {
+						sendErrorResponse(w, err.Error())
+						return
+					}
+				}
+
+				//OK! Unzip to destination
+				err := fs.Unzip(rsrcFile, rdestFile)
+				if err != nil {
+					sendErrorResponse(w, err.Error())
+					return
+				}
+
+			} else {
+				sendErrorResponse(w, "Unknown file opeartion given.")
+				return
+			}
+		}
+
+	}
+	sendJSONResponse(w, "\"OK\"")
+	return
+}
+
+//Allow systems to store key value pairs in the database as preferences.
+func system_fs_handleUserPreference(w http.ResponseWriter, r *http.Request) {
+	username, err := authAgent.GetUserName(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	key, _ := mv(r, "key", false)
+	value, _ := mv(r, "value", false)
+	if key != "" && value == "" {
+		//Get mode. Read the prefernece with given key
+		result := ""
+		err := sysdb.Read("fs", "pref/"+key+"/"+username, &result)
+		if err != nil {
+			sendJSONResponse(w, "{\"error\":\"Key not found.\"}")
+			return
+		}
+		sendTextResponse(w, result)
+	} else if key != "" && value != "" {
+		//Set mode. Set the preference with given key
+		sysdb.Write("fs", "pref/"+key+"/"+username, value)
+		sendJSONResponse(w, "\"OK\"")
+	}
+}
+
+func system_fs_removeUserPreferences(username string) {
+	entries, err := sysdb.ListTable("fs")
+	if err != nil {
+		return
+	}
+
+	for _, keypairs := range entries {
+		if strings.Contains(string(keypairs[0]), "pref/") && strings.Contains(string(keypairs[0]), "/"+username) {
+			//Remove this preference
+			sysdb.Delete("fs", string(keypairs[0]))
+		}
+	}
+}
+
+func system_fs_listDrives(w http.ResponseWriter, r *http.Request) {
+	if authAgent.CheckAuth(r) == false {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+	userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
+	type driveInfo struct {
+		Drivepath       string
+		DriveFreeSpace  uint64
+		DriveTotalSpace uint64
+		DriveAvailSpace uint64
+	}
+	var drives []driveInfo
+	if runtime.GOOS == "windows" {
+		//Under windows
+		for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
+			f, err := os.Open(string(drive) + ":\\")
+			if err == nil {
+				thisdrive := new(driveInfo)
+				thisdrive.Drivepath = string(drive) + ":\\"
+				free, total, avail := storage.GetDriveCapacity(string(drive) + ":\\")
+				thisdrive.DriveFreeSpace = free
+				thisdrive.DriveTotalSpace = total
+				thisdrive.DriveAvailSpace = avail
+				drives = append(drives, *thisdrive)
+				f.Close()
+			}
+		}
+	} else {
+		//Under linux environment
+		//Append all the virtual directories root as root instead
+		storageDevices := []string{}
+		for _, fshandler := range userinfo.GetAllFileSystemHandler() {
+			storageDevices = append(storageDevices, fshandler.Path)
+		}
+
+		//List all storage information of each devices
+		for _, dev := range storageDevices {
+			thisdrive := new(driveInfo)
+			thisdrive.Drivepath = filepath.Base(dev)
+			free, total, avail := storage.GetDriveCapacity(string(dev))
+			thisdrive.DriveFreeSpace = free
+			thisdrive.DriveTotalSpace = total
+			thisdrive.DriveAvailSpace = avail
+			drives = append(drives, *thisdrive)
+		}
+
+	}
+
+	jsonString, _ := json.Marshal(drives)
+	sendJSONResponse(w, string(jsonString))
+}
+
+func system_fs_listRoot(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	username := userinfo.Username
+	userRoot, _ := mv(r, "user", false)
+	if userRoot == "true" {
+		type fileObject struct {
+			Filename string
+			Filepath string
+			IsDir    bool
+		}
+		//List the root media folders under user:/
+		var filesInUserRoot []fileObject
+		filesInRoot, _ := filepath.Glob(*root_directory + "users/" + username + "/*")
+		for _, file := range filesInRoot {
+			thisFile := new(fileObject)
+			thisFile.Filename = filepath.Base(file)
+			thisFile.Filepath, _ = userinfo.RealPathToVirtualPath(file)
+			thisFile.IsDir = IsDir(file)
+			filesInUserRoot = append(filesInUserRoot, *thisFile)
+		}
+		jsonString, _ := json.Marshal(filesInUserRoot)
+		sendJSONResponse(w, string(jsonString))
+	} else {
+		type rootObject struct {
+			RootName string
+			RootPath string
+		}
+		var roots []rootObject
+		for _, store := range userinfo.GetAllFileSystemHandler() {
+			var thisDevice = new(rootObject)
+			thisDevice.RootName = store.Name
+			thisDevice.RootPath = store.UUID + ":/"
+			roots = append(roots, *thisDevice)
+		}
+		jsonString, _ := json.Marshal(roots)
+		sendJSONResponse(w, string(jsonString))
+	}
+
+}
+
+/*
+	Special Glob for handling path with [ or ] inside.
+	You can also pass in normal path for globing if you are not sure.
+*/
+
+func system_fs_specialGlob(path string) ([]string, error) {
+	files, err := filepath.Glob(path)
+	if err != nil {
+		return []string{}, err
+	}
+
+	if strings.Contains(path, "[") == true || strings.Contains(path, "]") == true {
+		if len(files) == 0 {
+			//Handle reverse check. Replace all [ and ] with *
+			newSearchPath := strings.ReplaceAll(path, "[", "?")
+			newSearchPath = strings.ReplaceAll(newSearchPath, "]", "?")
+			//Scan with all the similar structure except [ and ]
+			tmpFilelist, _ := filepath.Glob(newSearchPath)
+			for _, file := range tmpFilelist {
+				file = filepath.ToSlash(file)
+				if strings.Contains(file, filepath.ToSlash(filepath.Dir(path))) {
+					files = append(files, file)
+				}
+			}
+		}
+	}
+	//Convert all filepaths to slash
+	for i := 0; i < len(files); i++ {
+		files[i] = filepath.ToSlash(files[i])
+	}
+	return files, nil
+}
+
+func system_fs_specialURIDecode(inputPath string) string {
+	inputPath = strings.ReplaceAll(inputPath, "+", "{{plus_sign}}")
+	inputPath, _ = url.QueryUnescape(inputPath)
+	inputPath = strings.ReplaceAll(inputPath, "{{plus_sign}}", "+")
+	return inputPath
+}
+
+func system_fs_specialURIEncode(inputPath string) string {
+	inputPath = strings.ReplaceAll(inputPath, " ", "{{space_sign}}")
+	inputPath, _ = url.QueryUnescape(inputPath)
+	inputPath = strings.ReplaceAll(inputPath, "{{space_sign}}", "%20")
+	return inputPath
+}
+
+func system_fs_matchFileExt(inputFilename string, extArray []string) bool {
+	inputExt := filepath.Ext(inputFilename)
+	if stringInSlice(inputExt, extArray) {
+		return true
+	}
+	return false
+}
+
+//Handle file properties request
+func system_fs_getFileProperties(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	vpath, err := mv(r, "path", true)
+	if err != nil {
+		sendErrorResponse(w, "path not defined")
+		return
+	}
+
+	rpath, err := userinfo.VirtualPathToRealPath(vpath)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	fileStat, err := os.Stat(rpath)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	type fileProperties struct {
+		VirtualPath    string
+		StoragePath    string
+		Basename       string
+		VirtualDirname string
+		StorageDirname string
+		Ext            string
+		MimeType       string
+		Filesize       int64
+		Permission     string
+		LastModTime    string
+		LastModUnix    int64
+		IsDirectory    bool
+		Owner          string
+	}
+
+	mime := "text/directory"
+	if !fileStat.IsDir() {
+		m, _, err := fs.GetMime(rpath)
+		if err != nil {
+			mime = ""
+		}
+		mime = m
+	}
+
+	filesize := fileStat.Size()
+	//Get file overall size if this is folder
+	if fileStat.IsDir() {
+		var size int64
+		filepath.Walk(rpath, func(_ string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if !info.IsDir() {
+				size += info.Size()
+			}
+			return err
+		})
+		filesize = size
+	}
+
+	//Get file owner
+	owner := userinfo.GetFileOwner(rpath)
+	if owner == "" {
+		owner = "Unknown"
+	}
+
+	result := fileProperties{
+		VirtualPath:    vpath,
+		StoragePath:    filepath.Clean(rpath),
+		Basename:       filepath.Base(rpath),
+		VirtualDirname: filepath.ToSlash(filepath.Dir(vpath)),
+		StorageDirname: filepath.ToSlash(filepath.Dir(rpath)),
+		Ext:            filepath.Ext(rpath),
+		MimeType:       mime,
+		Filesize:       filesize,
+		Permission:     fileStat.Mode().Perm().String(),
+		LastModTime:    timeToString(fileStat.ModTime()),
+		LastModUnix:    fileStat.ModTime().Unix(),
+		IsDirectory:    fileStat.IsDir(),
+		Owner:          owner,
+	}
+
+	jsonString, _ := json.Marshal(result)
+	sendJSONResponse(w, string(jsonString))
+}
+
+/*
+	List directory in the given path
+
+	Usage: Pass in dir like the following examples:
+	AOR:/Desktop	<= Open /user/{username}/Desktop
+	S1:/			<= Open {uuid=S1}/
+
+
+*/
+
+func system_fs_handleList(w http.ResponseWriter, r *http.Request) {
+
+	currentDir, _ := mv(r, "dir", true)
+	currentDir, _ = url.QueryUnescape(currentDir)
+	sortMode, _ := mv(r, "sort", true)
+	showHidden, _ := mv(r, "showHidden", true)
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		//user not logged in. Redirect to login page.
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	if currentDir == "" {
+		sendErrorResponse(w, "Invalid dir given.")
+		return
+	}
+
+	//Pad a slash at the end of currentDir if not exists
+	if currentDir[len(currentDir)-1:] != "/" {
+		currentDir = currentDir + "/"
+	}
+	//Convert the virutal path to realpath
+	realpath, err := userinfo.VirtualPathToRealPath(currentDir)
+	//log.Println(realpath)
+	if err != nil {
+		sendErrorResponse(w, "Error. Unable to parse path. "+err.Error())
+		return
+	}
+	if !fileExists(realpath) {
+		userRoot, _ := userinfo.VirtualPathToRealPath("user:/")
+		if filepath.Clean(realpath) == filepath.Clean(userRoot) {
+			//Initiate user folder (Initiaed in user object)
+			userinfo.GetHomeDirectory()
+		} else {
+			//Folder not exists
+			sendJSONResponse(w, "{\"error\":\"Folder not exists\"}")
+			return
+		}
+
+	}
+	if sortMode == "" {
+		sortMode = "default"
+	}
+
+	//Check for really special exception in where the path contains [ or ] which cannot be handled via Golang Glob function
+	files, _ := system_fs_specialGlob(filepath.Clean(realpath) + "/*")
+
+	var parsedFilelist []fs.FileData
+
+	for _, v := range files {
+		if showHidden != "true" && filepath.Base(v)[:1] == "." {
+			//Skipping hidden files
+			continue
+		}
+		rawsize := fs.GetFileSize(v)
+		modtime, _ := fs.GetModTime(v)
+		thisFile := fs.FileData{
+			Filename:    filepath.Base(v),
+			Filepath:    currentDir + filepath.Base(v),
+			Realpath:    v,
+			IsDir:       IsDir(v),
+			Filesize:    rawsize,
+			Displaysize: fs.GetFileDisplaySize(rawsize, 2),
+			ModTime:     modtime,
+			IsShared:    shareManager.FileIsShared(v),
+		}
+
+		parsedFilelist = append(parsedFilelist, thisFile)
+	}
+
+	//Sort the filelist
+	if sortMode == "default" {
+		//Sort by name, convert filename to window sorting methods
+		sort.Slice(parsedFilelist, func(i, j int) bool {
+			return strings.ToLower(parsedFilelist[i].Filename) < strings.ToLower(parsedFilelist[j].Filename)
+		})
+	} else if sortMode == "reverse" {
+		//Sort by reverse name
+		sort.Slice(parsedFilelist, func(i, j int) bool {
+			return strings.ToLower(parsedFilelist[i].Filename) > strings.ToLower(parsedFilelist[j].Filename)
+		})
+	} else if sortMode == "smallToLarge" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].Filesize < parsedFilelist[j].Filesize })
+	} else if sortMode == "largeToSmall" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].Filesize > parsedFilelist[j].Filesize })
+	} else if sortMode == "mostRecent" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].ModTime > parsedFilelist[j].ModTime })
+	} else if sortMode == "leastRecent" {
+		sort.Slice(parsedFilelist, func(i, j int) bool { return parsedFilelist[i].ModTime < parsedFilelist[j].ModTime })
+	}
+
+	jsonString, _ := json.Marshal(parsedFilelist)
+	sendJSONResponse(w, string(jsonString))
+
+}
+
+//Handle getting a hash from a given contents in the given path
+func system_fs_handleDirHash(w http.ResponseWriter, r *http.Request) {
+	currentDir, err := mv(r, "dir", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid dir given")
+		return
+	}
+
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	rpath, err := userinfo.VirtualPathToRealPath(currentDir)
+	if err != nil {
+		sendErrorResponse(w, "Invalid dir given")
+		return
+	}
+
+	//Get a list of files in this directory
+	currentDir = filepath.ToSlash(filepath.Clean(rpath)) + "/"
+	filesInDir, err := system_fs_specialGlob(currentDir + "*")
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	filenames := []string{}
+	for _, file := range filesInDir {
+		if len(filepath.Base(file)) > 0 && string([]rune(filepath.Base(file))[0]) != "." {
+			//Ignore hidden files
+			filenames = append(filenames, filepath.Base(file))
+		}
+
+	}
+
+	sort.Strings(filenames)
+
+	//Build a hash base on the filelist
+	h := sha256.New()
+	h.Write([]byte(strings.Join(filenames, ",")))
+	sendTextResponse(w, hex.EncodeToString((h.Sum(nil))))
+}
+
+/*
+	File zipping and unzipping functions
+*/
+
+//Handle all zip related API
+func system_fs_zipHandler(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	opr, err := mv(r, "opr", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid opr or opr not defined")
+		return
+	}
+
+	vsrc, _ := mv(r, "src", true)
+	if vsrc == "" {
+		sendErrorResponse(w, "Invalid src paramter")
+		return
+	}
+
+	vdest, _ := mv(r, "dest", true)
+	rdest := ""
+
+	//Convert source path from JSON string to object
+	virtualSourcePaths := []string{}
+	err = json.Unmarshal([]byte(vsrc), &virtualSourcePaths)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Check each of the path
+	realSourcePaths := []string{}
+	for _, vpath := range virtualSourcePaths {
+		thisrpath, err := userinfo.VirtualPathToRealPath(vpath)
+		if err != nil || !fileExists(thisrpath) {
+			sendErrorResponse(w, "File not exists: "+vpath)
+			return
+		}
+		realSourcePaths = append(realSourcePaths, thisrpath)
+	}
+
+	///Convert dest to real if given
+	if vdest != "" {
+		realdest, _ := userinfo.VirtualPathToRealPath(vdest)
+		rdest = realdest
+	}
+
+	//This function will be deprecate soon in ArozOS 1.120
+	log.Println("*DEPRECATE* zipHandler will be deprecating soon! Please use fileOpr endpoint")
+
+	if opr == "zip" {
+		//Check if destination location exists
+		if rdest == "" || !fileExists(filepath.Dir(rdest)) {
+			sendErrorResponse(w, "Invalid dest location")
+			return
+		}
+
+		//OK. Create the zip at the desired location
+		err := fs.ArozZipFile(realSourcePaths, rdest, false)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+
+		sendOK(w)
+	} else if opr == "tmpzip" {
+		//Zip to tmp folder
+		userTmpFolder, _ := userinfo.VirtualPathToRealPath("tmp:/")
+		filename := Int64ToString(GetUnixTime()) + ".zip"
+		rdest := filepath.ToSlash(filepath.Clean(userTmpFolder)) + "/" + filename
+
+		log.Println(realSourcePaths, rdest)
+		err := fs.ArozZipFile(realSourcePaths, rdest, false)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Send the tmp filename to the user
+		sendTextResponse(w, "tmp:/"+filename)
+
+	} else if opr == "inspect" {
+
+	} else if opr == "unzip" {
+
+	}
+
+}
+
+//Translate path from and to virtual and realpath
+func system_fs_handlePathTranslate(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	path, err := mv(r, "path", false)
+	if err != nil {
+		sendErrorResponse(w, "Invalid path given")
+		return
+	}
+	rpath, err := userinfo.VirtualPathToRealPath(path)
+	if err != nil {
+		//Try to convert it to virtualPath
+		vpath, err := userinfo.RealPathToVirtualPath(path)
+		if err != nil {
+			sendErrorResponse(w, "Unknown path given")
+		} else {
+			jsonstring, _ := json.Marshal(vpath)
+			sendJSONResponse(w, string(jsonstring))
+		}
+	} else {
+		abrpath, _ := filepath.Abs(rpath)
+		jsonstring, _ := json.Marshal([]string{rpath, filepath.ToSlash(abrpath)})
+		sendJSONResponse(w, string(jsonstring))
+	}
+
+}
+
+//Handle cache rendering with websocket pipeline
+func system_fs_handleCacheRender(w http.ResponseWriter, r *http.Request) {
+	userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
+	vpath, err := mv(r, "folder", false)
+	if err != nil {
+		sendErrorResponse(w, "Invalid folder paramter")
+		return
+	}
+
+	//Convert vpath to realpath
+	rpath, err := userinfo.VirtualPathToRealPath(vpath)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Perform cache rendering
+	thumbRenderHandler.HandleLoadCache(w, r, rpath)
+
+}
+
+//Handle file thumbnail caching
+func system_fs_handleFolderCache(w http.ResponseWriter, r *http.Request) {
+	userinfo, _ := userHandler.GetUserInfoFromRequest(w, r)
+	vfolderpath, err := mv(r, "folder", false)
+	if err != nil {
+		sendErrorResponse(w, "folder not defined")
+		return
+	}
+
+	rpath, err := userinfo.VirtualPathToRealPath(vfolderpath)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	thumbRenderHandler.BuildCacheForFolder(rpath)
+
+	sendOK(w)
+}
+
+//Functions for handling quick file write without the need to go through agi for simple apps
+func system_fs_handleFileWrite(w http.ResponseWriter, r *http.Request) {
+	//Get the username for this user
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Get the file content and the filepath
+	content, _ := mv(r, "content", true)
+	targetFilepath, err := mv(r, "filepath", true)
+	if err != nil {
+		sendErrorResponse(w, "Filepath cannot be empty")
+		return
+	}
+
+	//Check if filepath is writable
+	if !userinfo.CanWrite(targetFilepath) {
+		sendErrorResponse(w, "Write permission denied")
+		return
+	}
+
+	//Convert the filepath to realpath
+	rpath, err := userinfo.VirtualPathToRealPath(targetFilepath)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Check if the path dir exists. If not, return error
+	if !fileExists(filepath.Dir(rpath)) {
+		sendErrorResponse(w, "Directory not exists")
+		return
+	}
+
+	//OK. Write to that file
+	err = ioutil.WriteFile(rpath, []byte(content), 0755)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	sendOK(w)
+
+}
+
+//Handle setting and loading of file permission on Linux
+func system_fs_handleFilePermission(w http.ResponseWriter, r *http.Request) {
+	file, err := mv(r, "file", true)
+	if err != nil {
+		sendErrorResponse(w, "Invalid file")
+		return
+	}
+
+	//Translate the file to real path
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+	rpath, err := userinfo.VirtualPathToRealPath(file)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	newMode, _ := mv(r, "mode", true)
+	if newMode == "" {
+		//Read the file mode
+
+		//Check if the file exists
+		if !fileExists(rpath) {
+			sendErrorResponse(w, "File not exists!")
+			return
+		}
+
+		//Read the file permission
+		filePermission, err := fsp.GetFilePermissions(rpath)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+
+		//Send the file permission to client
+		js, _ := json.Marshal(filePermission)
+		sendJSONResponse(w, string(js))
+	} else {
+		//Set the file mode
+		//Check if the file exists
+		if !fileExists(rpath) {
+			sendErrorResponse(w, "File not exists!")
+			return
+		}
+
+		//Check if windows. If yes, ignore this request
+		if runtime.GOOS == "windows" {
+			sendErrorResponse(w, "Windows host not supported")
+			return
+		}
+
+		//Check if this user has permission to change the file permission
+		//Aka user must be 1. This is his own folder or 2. Admin
+		fsh, _ := userinfo.GetFileSystemHandlerFromVirtualPath(file)
+		if fsh.Hierarchy == "user" {
+			//Always ok as this is owned by the user
+		} else if fsh.Hierarchy == "public" {
+			//Require admin
+			if userinfo.IsAdmin() == false {
+				sendErrorResponse(w, "Permission Denied")
+				return
+			}
+		} else {
+			//Not implemeneted. Require admin
+			if userinfo.IsAdmin() == false {
+				sendErrorResponse(w, "Permission Denied")
+				return
+			}
+		}
+
+		//Be noted that if the system is not running in sudo mode,
+		//File permission change might not works.
+
+		err := fsp.SetFilePermisson(rpath, newMode)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		} else {
+			sendOK(w)
+		}
+	}
+}
+
+//Check if the given filepath is and must inside the given directory path.
+//You can pass both as relative
+func system_fs_checkFileInDirectory(filesourcepath string, directory string) bool {
+	filepathAbs, err := filepath.Abs(filesourcepath)
+	if err != nil {
+		return false
+	}
+
+	directoryAbs, err := filepath.Abs(directory)
+	if err != nil {
+		return false
+	}
+
+	//Check if the filepathabs contain directoryAbs
+	if strings.Contains(filepathAbs, directoryAbs) {
+		return true
+	} else {
+		return false
+	}
+
+}
+
+//Clear the old files inside the tmp file
+func system_fs_clearOldTmpFiles() {
+	filesToBeDelete := []string{}
+	tmpAbs, _ := filepath.Abs(*tmp_directory)
+	filepath.Walk(*tmp_directory, func(path string, info os.FileInfo, err error) error {
+		if filepath.Base(path) != "aofs.db" && filepath.Base(path) != "aofs.db.lock" {
+			//Check if root folders. Do not delete root folders
+			parentAbs, _ := filepath.Abs(filepath.Dir(path))
+
+			if tmpAbs == parentAbs {
+				//Root folder. Do not remove
+				return nil
+			}
+			//Get its modification time
+			modTime, err := fs.GetModTime(path)
+			if err != nil {
+				return nil
+			}
+
+			//Check if mod time is more than 24 hours ago
+			if time.Now().Unix()-modTime > int64(*maxTempFileKeepTime) {
+				//Delete OK
+				filesToBeDelete = append(filesToBeDelete, path)
+			}
+		}
+		return nil
+	})
+
+	//Remove all files from the delete list
+	for _, fileToBeDelete := range filesToBeDelete {
+		os.RemoveAll(fileToBeDelete)
+	}
+
+}

BIN
flag.txt


+ 49 - 0
go.mod

@@ -0,0 +1,49 @@
+module imuslab.com/arozos
+
+go 1.13
+
+require (
+	github.com/andybalholm/brotli v1.0.0 // indirect
+	github.com/boltdb/bolt v1.3.1
+	github.com/dhowden/tag v0.0.0-20200828214007-46e57f75dbfc
+	github.com/disintegration/imaging v1.6.2
+	github.com/fclairamb/ftpserverlib v0.8.0
+	github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802
+	github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 // indirect
+	github.com/frankban/quicktest v1.10.0 // indirect
+	github.com/gabriel-vasile/mimetype v1.1.0
+	github.com/go-git/go-git/v5 v5.2.0
+	github.com/gorilla/schema v1.2.0
+	github.com/gorilla/sessions v1.2.0
+	github.com/gorilla/websocket v1.4.2
+	github.com/grandcat/zeroconf v1.0.0
+	github.com/jung-kurt/gofpdf v1.16.2
+	github.com/klauspost/compress v1.10.6 // indirect
+	github.com/klauspost/pgzip v1.2.4 // indirect
+	github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d
+	github.com/mholt/archiver/v3 v3.3.0
+	github.com/miekg/dns v1.1.29 // indirect
+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
+	github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
+	github.com/nwaples/rardecode v1.1.0 // indirect
+	github.com/oliamb/cutter v0.2.2
+	github.com/otiai10/copy v1.1.1
+	github.com/pierrec/lz4 v2.5.2+incompatible // indirect
+	github.com/ricochet2200/go-disk-usage v0.0.0-20150921141558-f0d1b743428f
+	github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff
+	github.com/satori/go.uuid v1.2.0
+	github.com/smartystreets/cproxy v1.0.2
+	github.com/smartystreets/logging v1.1.1 // indirect
+	github.com/spf13/afero v1.3.1
+	github.com/tidwall/pretty v1.0.2
+	github.com/ulikunitz/xz v0.5.7 // indirect
+	github.com/valyala/fasttemplate v1.1.0
+	gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
+	gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3
+	golang.org/x/net v0.0.0-20200625001655-4c5254603344
+	golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
+	golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
+	golang.org/x/text v0.3.3 // indirect
+	gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
+	gopkg.in/sourcemap.v1 v1.0.5 // indirect
+)

+ 547 - 0
go.sum

@@ -0,0 +1,547 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
+github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
+github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
+github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
+github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6 h1:bZ28Hqta7TFAK3Q08CMvv8y3/8ATaEqv2nGoc6yff6c=
+github.com/andybalholm/brotli v0.0.0-20190621154722-5f990b63d2d6/go.mod h1:+lx6/Aqd1kLJ1GQfkvOnaZ1WGmLpMpbprPuIOOZX30U=
+github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
+github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
+github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
+github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
+github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
+github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
+github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
+github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
+github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
+github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dhowden/tag v0.0.0-20200828214007-46e57f75dbfc h1:8ndBJ8cTZwp5Qtl5fnG5bM/ekMnm1GFdUSMTGiN09K0=
+github.com/dhowden/tag v0.0.0-20200828214007-46e57f75dbfc/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
+github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
+github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
+github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
+github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
+github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
+github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
+github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
+github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
+github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
+github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
+github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fclairamb/ftpserverlib v0.8.0 h1:ZsWUQ8Vg3Y8LIWRUAzVnFXY982Yztz2odDdK/UVJtik=
+github.com/fclairamb/ftpserverlib v0.8.0/go.mod h1:xF4cy07oCHA9ZorKehsFGqA/1UHYaonmqHK2g3P1X8U=
+github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
+github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802 h1:5vdq0jOnV15v1NdZbAcU+dIJ22rFgwaieiFewPvnKCA=
+github.com/fogleman/fauxgl v0.0.0-20200818143847-27cddc103802/go.mod h1:7f7F8EvO8MWvDx9sIoloOfZBCKzlWuZV/h3TjpXOO3k=
+github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046 h1:n3RPbpwXSFT0G8FYslzMUBDO09Ix8/dlqzvUkcJm4Jk=
+github.com/fogleman/simplify v0.0.0-20170216171241-d32f302d5046/go.mod h1:KDwyDqFmVUxUmo7tmqXtyaaJMdGon06y8BD2jmh84CQ=
+github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
+github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
+github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE=
+github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gabriel-vasile/mimetype v1.1.0 h1:+ahX+MvQPFve4kO9Qjjxf3j49i0ACdV236kJlOCRAnU=
+github.com/gabriel-vasile/mimetype v1.1.0/go.mod h1:6CDPel/o/3/s4+bp6kIbsWATq8pmgOisOPG40CJa6To=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
+github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4=
+github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
+github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM=
+github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
+github.com/go-git/go-git v1.0.0 h1:YcN9iDGDoXuIw0vHls6rINwV416HYa0EB2X+RBsyYp4=
+github.com/go-git/go-git v4.7.0+incompatible h1:+W9rgGY4DOKKdX2x6HxSR7HNeTxqiKrOvKnuittYVdA=
+github.com/go-git/go-git-fixtures/v4 v4.0.2-0.20200613231340-f56387b50c12/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
+github.com/go-git/go-git/v5 v5.2.0 h1:YPBLG/3UK1we1ohRkncLjaXWLW+HKp5QNM/jTli2JgI=
+github.com/go-git/go-git/v5 v5.2.0/go.mod h1:kh02eMX+wdqqxgNMEyq8YgwlIOsDOa9homkUq1PoTMs=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
+github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
+github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721 h1:KRMr9A3qfbVM7iV/WcLY/rL5LICqwMHLhwRXKu99fXw=
+github.com/golang/gddo v0.0.0-20190419222130-af0f2af80721/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
+github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
+github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
+github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
+github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
+github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
+github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
+github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
+github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
+github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
+github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
+github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
+github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
+github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
+github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.9.2 h1:LfVyl+ZlLlLDeQ/d2AqfGIIH4qEDu0Ed2S5GyhCWIWY=
+github.com/klauspost/compress v1.9.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.10.6 h1:SP6zavvTG3YjOosWePXFDlExpKIWMTO4SE/Y8MZB2vI=
+github.com/klauspost/compress v1.10.6/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
+github.com/klauspost/pgzip v1.2.1 h1:oIPZROsWuPHpOdMVWLuJZXwgjhrW8r1yEX8UqMyeNHM=
+github.com/klauspost/pgzip v1.2.1/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/klauspost/pgzip v1.2.4 h1:TQ7CNpYKovDOmqzRHKxJh0BeaBI7UdQZYc6p7pMQh1A=
+github.com/klauspost/pgzip v1.2.4/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d h1:68u9r4wEvL3gYg2jvAOgROwZ3H+Y3hIDk4tbbmIjcYQ=
+github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
+github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
+github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mholt/archiver/v3 v3.3.0 h1:vWjhY8SQp5yzM9P6OJ/eZEkmi3UAbRrxCq48MxjAzig=
+github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08Ebtr1Mqao=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
+github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
+github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
+github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
+github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
+github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nwaples/rardecode v1.0.0 h1:r7vGuS5akxOnR4JQSkko62RJ1ReCMXxQRPtxsiFMBOs=
+github.com/nwaples/rardecode v1.0.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
+github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
+github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
+github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
+github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
+github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
+github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k=
+github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
+github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
+github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
+github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
+github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
+github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
+github.com/otiai10/copy v1.1.1 h1:PH7IFlRQ6Fv9vYmuXbDRLdgTHoP1w483kPNUP2bskpo=
+github.com/otiai10/copy v1.1.1/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
+github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
+github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc=
+github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
+github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
+github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
+github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
+github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
+github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
+github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI=
+github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
+github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/ricochet2200/go-disk-usage v0.0.0-20150921141558-f0d1b743428f h1:w4VLAgWDnrcBDFSi8Ppn/MrB/Z1A570+MV90CvMtVVA=
+github.com/ricochet2200/go-disk-usage v0.0.0-20150921141558-f0d1b743428f/go.mod h1:yhevTRDiduxPJHQDCtlqUn53ojFPkRh/mKhMUzQUCpc=
+github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff h1:+6NUiITWwE5q1KO6SAfUX918c+Tab0+tGAM/mtdlUyA=
+github.com/robertkrimen/otto v0.0.0-20191219234010-c382bd3c16ff/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/secsy/goftp v0.0.0-20190720192957-f31499d7c79a h1:C6IhVTxNkhlb0tlCB6JfHOUv1f0xHPK7V8X4HlJZEJw=
+github.com/secsy/goftp v0.0.0-20190720192957-f31499d7c79a/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
+github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/assertions v1.0.1 h1:voD4ITNjPL5jjBfgR/r8fPIIBrliWrWHeiJApdr3r4w=
+github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
+github.com/smartystreets/cproxy v1.0.2 h1:72JRJIx975Tk0yPQrZ9YOFhiMSIAdp8ljxmpc7OoNu8=
+github.com/smartystreets/cproxy v1.0.2/go.mod h1:qDRe8RO2GQUwJ3rt8wukdFrb5hcaI0YKpykWZ2W9wuI=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/smartystreets/gunit v1.1.3 h1:32x+htJCu3aMswhPw3teoJ+PnWPONqdNgaGs6Qt8ZaU=
+github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
+github.com/smartystreets/logging v1.0.2 h1:ScLtqaKBnjIyTZw218wCALtb4RMMISWsk9xz1QP7EjM=
+github.com/smartystreets/logging v1.0.2/go.mod h1:66odR4LeIkc7lNzxhS+ktkvoSUhsyTM4e0NSXI9Bmac=
+github.com/smartystreets/logging v1.1.1 h1:4UlnyYWB7LDd216NTuP3zTVvMQZREtPrDnJbsz0zftI=
+github.com/smartystreets/logging v1.1.1/go.mod h1:NwFCEPbtiTIug+UCtTbDVjgcJNfandE3brDx0x7QEY8=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
+github.com/spf13/afero v1.3.1 h1:GPTpEAuNr98px18yNQ66JllNil98wfRZ/5Ukny8FeQA=
+github.com/spf13/afero v1.3.1/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
+github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
+github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
+github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
+github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
+github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
+github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
+github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
+github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
+github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs=
+gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA=
+gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3 h1:qXqiXDgeQxspR3reot1pWme00CX1pXbxesdzND+EjbU=
+gitlab.com/NebulousLabs/go-upnp v0.0.0-20181011194642-3a71999ed0d3/go.mod h1:sleOmkovWsDEQVYXmOJhx69qheoMTmCuPYyiCFCihlg=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
+golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
+gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14 h1:tHqNpm9sPaE6BSuMLXBzgTwukQLdBEt4OYU2coQjEQQ=
+gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14/go.mod h1:nzmlZQ+UqB5+55CRTV/dOaiK8OrPl6Co96Ob8lH4Wxw=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
+gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
+sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

+ 134 - 0
hardware.power.go

@@ -0,0 +1,134 @@
+package main
+
+import (
+	"net/http"
+	"log"
+
+	"os/exec"
+	"runtime"
+)
+
+func HardwarePowerInit(){
+	if (*allow_hardware_management){
+		//Only register these paths when hardware management is enabled
+		http.HandleFunc("/system/power/shutdown", hardware_power_poweroff)
+		http.HandleFunc("/system/power/restart", hardware_power_restart)
+	}
+
+	http.HandleFunc("/system/power/accessCheck", hardware_power_checkIfHardware)
+}
+
+func hardware_power_checkIfHardware(w http.ResponseWriter, r *http.Request){
+	if (*allow_hardware_management){
+		sendJSONResponse(w, "true")
+	}else{
+		sendJSONResponse(w, "false")
+	}
+}
+
+func hardware_power_poweroff(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w,r)
+	if err != nil{
+		w.WriteHeader(http.StatusUnauthorized)
+		w.Write([]byte("401 Unauthorized"))
+		return
+	}
+
+	if !userinfo.IsAdmin() {
+		sendErrorResponse(w, "Permission Denied")
+		return
+	}
+
+	if !sudo_mode {
+		sendErrorResponse(w, "Sudo mode required")
+		return
+	}
+
+	if runtime.GOOS == "windows" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("shutdown", "-s", "-t", "20")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "linux" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("/sbin/shutdown")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "darwin" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("sudo", "shutdown", "-h", "+1")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	sendOK(w)
+}
+
+func hardware_power_restart(w http.ResponseWriter, r *http.Request) {
+	userinfo, err := userHandler.GetUserInfoFromRequest(w,r)
+	if err != nil{
+		w.WriteHeader(http.StatusUnauthorized)
+		w.Write([]byte("401 Unauthorized"))
+		return
+	}
+
+	if !userinfo.IsAdmin() {
+		sendErrorResponse(w, "Permission Denied")
+		return
+	}
+
+	if !sudo_mode {
+		sendErrorResponse(w, "Sudo mode required")
+		return
+	}
+
+	if runtime.GOOS == "windows" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("shutdown", "-r", "-t", "20")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "linux" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("systemctl", "reboot")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "darwin" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("shutdown", "-r", "+1")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+	sendOK(w)
+}

+ 23 - 0
install-go.sh

@@ -0,0 +1,23 @@
+#/bin/bash
+echo "Input the go arch to install (arm/arm64/amd64)"
+read -p "Architecture: " arch
+if [ "$arch" = "arm" ]; then
+   echo "Installing arm version of go"
+   wget https://golang.org/dl/go1.15.3.linux-armv6l.tar.gz
+fi
+
+if [ "$arch" = "arm64" ]; then
+   echo "Installing arm64 version of go"
+   wget https://golang.org/dl/go1.15.3.linux-arm64.tar.gz
+fi
+
+if [ "$arch" = "amd64" ]; then
+   echo "Installing amd64 version of go"
+   wget https://golang.org/dl/go1.15.3.linux-amd64.tar.gz
+fi
+
+sudo tar -C /usr/local -xzf go*
+echo "ADD THE FOLLOWING LINE TO YOUR ~/.bashrc FILE:"
+echo 'export PATH=$PATH:/usr/local/go/bin'
+
+echo "Install Complted"

+ 248 - 0
legacy/disabled/auth.go

@@ -0,0 +1,248 @@
+package main
+
+import (
+    "net/http"
+    "strings"
+    "log"
+    "encoding/json"
+
+    "imuslab.com/arozos/mod/auth"
+)
+
+
+var (
+    // key must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256)
+    key = []byte("super-secret-key") //To be migrated to input flags
+)
+
+
+/*
+    Initiation of web services endpoints from main()
+
+    This function should be the preparation of auth services and register the url for auth services only
+    Do not put in any computational algorithms
+*/
+func authRegisterHandlerEndpoints(authAgent *auth.AuthAgent){
+    //Initiate auth services with system database
+    authAgent = auth.NewAuthenticationAgent("ao_auth", key, sysdb)
+
+    //Handle auth API
+    http.HandleFunc("/system/auth/login", authAgent.HandleLogin)
+    http.HandleFunc("/system/auth/logout", authAgent.HandleLogout)
+    http.HandleFunc("/system/auth/checkLogin", authAgent.CheckLogin)
+    http.HandleFunc("/system/auth/register", authAgent.HandleRegister)  //Require implemtantion of group check
+    http.HandleFunc("/system/auth/unregister", authAgent.HandleUnregister) //Require implementation of admin check
+    
+    //Handle other related APUs
+    http.HandleFunc("/system/auth/reflectIP", system_auth_getIPAddress)
+    http.HandleFunc("/system/auth/checkPublicRegister", system_auth_checkPublicRegister)
+    log.Println("ArOZ Online Authentication Service Loaded");
+
+
+    if (*allow_public_registry){
+        //Allow public registry. Create setting interface for this page
+        registerSetting(settingModule{
+            Name:     "Public Register",
+            Desc:     "Settings for public registration",
+            IconPath: "SystemAO/auth/img/small_icon.png",
+            Group:    "Users",
+            StartDir: "SystemAO/auth/regsetting.html",
+            RequireAdmin: true,
+        })
+
+
+        //Register the direct link for template serving
+        http.HandleFunc("/public/register", system_auth_serveRegisterInterface);
+        http.HandleFunc("/public/register/settings", system_auth_handleRegisterInterfaceUpdate);
+    }
+}
+
+func system_auth_checkPublicRegister(w http.ResponseWriter, r *http.Request){
+    if (!*allow_public_registry){
+        sendJSONResponse(w, "false");
+        return
+    }else{
+        AllowPublicRegisterValue := false
+        sysdb.Read("auth", "public/register/settings/allowRegistry", &AllowPublicRegisterValue)
+        jsonString, _ := json.Marshal(AllowPublicRegisterValue)
+        sendJSONResponse(w, string(jsonString))
+        return
+    }
+    sendJSONResponse(w, "false");
+}
+
+func system_auth_serveRegisterInterface(w http.ResponseWriter, r *http.Request){
+    username, err := mv(r, "username", true)
+    if (err != nil){
+        //Serve WebUI
+        //Prepare contents for templating
+        base64Image, _ := LoadImageAsBase64("./web/" + iconVendor)
+        requireInvitationCode := false
+        sysdb.Read("auth", "public/register/settings/enableInvitationCode", &requireInvitationCode)
+        eic := "false"
+        if (requireInvitationCode){
+            eic = "true"
+        }
+        //registerUI, _ := ioutil.ReadFile("./web/" + "SystemAO/auth/register.system");
+        registerUI, _ := template_load("./web/" + "SystemAO/auth/register.system",map[string]interface{}{
+            "vendor_logo": base64Image,
+            "host_name": *host_name,
+            "require_invitationCode": eic,
+        })
+        w.Write([]byte(registerUI))
+    }else{
+        //Data incoming. Register this user if data is valid
+        requireInvitationCode := false
+        sysdb.Read("auth", "public/register/settings/enableInvitationCode", &requireInvitationCode)
+
+        //Validate Invitation Code if enabled
+        if (requireInvitationCode){
+            //Validate the Invitation Code
+            userInputCode, _ := mv(r, "invitationcode", true)
+            correctCode := ""
+            sysdb.Read("auth", "public/register/settings/invitationCode", &correctCode)
+            if (correctCode == ""){
+                panic("Invalid Invitation Code")
+            }
+            if (userInputCode != correctCode){
+                sendErrorResponse(w, "Invalid Invitation Code")
+                return
+            }
+        }
+
+        //validate if this username already occupied
+        if authAgent.UserExists(username){
+            sendErrorResponse(w, "This username already occupied.")
+            return
+        }
+
+        //Validate password
+        password, err := mv(r, "password", true)
+        if (err != nil){
+            sendErrorResponse(w, "Invalid password")
+            return
+        }
+
+        if len(password) < 8{
+            sendErrorResponse(w, "Password too short. Password must be equal or longer than 8 characters")
+            return
+        }
+
+        //Validate default usergroup
+        DefaultUserGroupValue := ""
+        err = sysdb.Read("auth", "public/register/settings/defaultUserGroup", &DefaultUserGroupValue)
+        if (err != nil){
+            log.Println(err.Error())
+            sendErrorResponse(w, "Internal Server Error")
+            return
+        }
+
+        /*
+        if (DefaultUserGroupValue == "" || !system_permission_groupExists(DefaultUserGroupValue)){
+            log.Println("Invalid group given or group not exists: " + DefaultUserGroupValue)
+            sendErrorResponse(w, "Internal Server Error")
+            return
+        }
+        */
+
+        //Ok to create user
+        err = authAgent.CreateUserAccount(username, password, DefaultUserGroupValue)
+        if (err != nil){
+            log.Println(err.Error())
+            sendErrorResponse(w, "Internal Server Error")
+            return
+        }
+        sendOK(w);
+    }
+    
+}
+
+func system_auth_handleRegisterInterfaceUpdate(w http.ResponseWriter, r *http.Request){
+    /*
+    isAdmin := system_permission_checkUserIsAdmin(w,r)
+    if !isAdmin{
+        sendErrorResponse(w, "Permission denied")
+        return
+    }
+    */
+    
+    //keys for access the properties
+    var (
+        rootKey string = "public/register/settings/"
+        allowPublicRegister string = rootKey + "allowRegistry"
+        enableInvitationCode string = rootKey + "enableInvitationCode"
+        invitationCode string = rootKey + "invitationCode"
+        defaultUserGroup string = rootKey + "defaultUserGroup"
+    )
+
+    opr, _ := mv(r,"opr",true);
+    if (opr == "write"){
+        //Write settings to db
+        config, err := mv(r,"config",true);
+        if err != nil{
+            sendErrorResponse(w, "config not defined");
+            return
+        }
+
+        type configStruct struct {
+            Apr   bool   `json:"apr"`
+            Eivc  bool   `json:"eivc"`
+            Icode string `json:"icode"`
+            Group string `json:"group"`
+        }
+
+        newConfig := new(configStruct)
+        err = json.Unmarshal([]byte(config), &newConfig)
+        if (err != nil){
+            sendErrorResponse(w, err.Error())
+            return
+        }
+
+        /*
+        if (newConfig.Group == "" || !system_permission_groupExists(newConfig.Group)){
+            //Group is not set. Reject update
+            sendErrorResponse(w, "Invalid group selected");
+            return
+        }
+        */
+
+        //Write the configuration to file
+        sysdb.Write("auth", allowPublicRegister, newConfig.Apr)
+        sysdb.Write("auth", enableInvitationCode, newConfig.Eivc)
+        sysdb.Write("auth", invitationCode, newConfig.Icode)
+        sysdb.Write("auth", defaultUserGroup, newConfig.Group)
+
+        sendOK(w)
+    }else{
+        //Read the current settings
+        type replyStruct struct{
+            AllowPublicRegister bool
+            EnableInvitationCode bool
+            InvitationCode string
+            DefaultUserGroup string
+        }
+
+        var AllowPublicRegisterValue bool = false
+        var EnableInvitationCodeValue bool = false
+        var InvitationCodeValue string = ""
+        var DefaultUserGroupValue string = ""
+
+        sysdb.Read("auth", allowPublicRegister, &AllowPublicRegisterValue)
+        sysdb.Read("auth", enableInvitationCode, &EnableInvitationCodeValue)
+        sysdb.Read("auth", invitationCode, &InvitationCodeValue)
+        sysdb.Read("auth", defaultUserGroup, &DefaultUserGroupValue)
+
+        jsonString, _ := json.Marshal(replyStruct{
+            AllowPublicRegister:AllowPublicRegisterValue,
+            EnableInvitationCode:EnableInvitationCodeValue,
+            InvitationCode:InvitationCodeValue,
+            DefaultUserGroup:DefaultUserGroupValue,
+        })
+
+        sendJSONResponse(w, string(jsonString))
+    }
+}
+
+
+
+

+ 47 - 0
legacy/disabled/hardware.power.go

@@ -0,0 +1,47 @@
+package main
+
+import (
+	"net/http"
+	"log"
+)
+
+func hardware_power_init(){
+	if (*allow_hardware_management){
+		//Only register these paths when hardware management is enabled
+
+	}
+
+	http.HandleFunc("/system/power/accessCheck", hardware_power_checkIfHardware)
+}
+
+func hardware_power_checkIfHardware(w http.ResponseWriter, r *http.Request){
+	if (*allow_hardware_management){
+		sendJSONResponse(w, "true")
+	}else{
+		sendJSONResponse(w, "false")
+	}
+}
+
+//Pass in shutdown={deviceuuid} to shutdown
+func hardware_power_restart(w http.ResponseWriter, r *http.Request){
+	_, err := authAgent.GetUserName(w,r);
+	if (err != nil){
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+	isAdmin := system_permission_checkUserIsAdmin(w,r)
+	if (!isAdmin){
+		sendErrorResponse(w, "Permission denied")
+		return
+	}
+
+	poweroff, _ := mv(r, "shutdown", true)
+	if (poweroff == ""){
+		//Do system restart
+		log.Println("Restarting");
+	}else if (poweroff == deviceUUID){
+		//Do system shutdown
+		log.Println("Shutting down");
+	}
+}
+

+ 25 - 0
legacy/disabled/language.go

@@ -0,0 +1,25 @@
+package main
+
+import (
+
+)
+
+func system_lang_getSystemLanuage(){
+
+}
+
+func system_lang_setSystemLanuage(){
+
+}
+
+func system_lang_getLocalization(tranfile string, key string){
+	
+}
+
+/*
+	Translation Indexing functions
+
+	The following functions will do the template and indexing for lanuage translation from config files.
+
+*/
+

+ 326 - 0
legacy/disabled/module.Music.go

@@ -0,0 +1,326 @@
+package main
+
+import (
+	"net/http"
+	"log"
+	"strings"
+	"path/filepath"
+	"encoding/json"
+	"os"
+	"fmt"
+	"strconv"
+)
+
+/*
+	AirMUsic - Maybe the best music playback app on ArOZ Online 
+
+	CopyRight Toby Chui, 2020
+*/
+
+func module_Music_init(){
+	http.HandleFunc("/Music/listSong", module_airMusic_listSong)
+	http.HandleFunc("/Music/getMeta", module_airMusic_getMeta)
+	http.HandleFunc("/Music/getFileInfo", module_airMusic_getFileInfo)
+	
+
+	//Register this module to system
+	registerModule(moduleInfo{
+		Name: "Music",
+		Desc: "The basic music player for ArOZ Online",
+		Group: "Media",
+		IconPath: "Music/img/module_icon.png",
+		Version: "0.0.4",
+		StartDir: "Music/index.html",
+		SupportFW: true,
+		LaunchFWDir: "Music/index.html",
+		SupportEmb: true,
+		LaunchEmb: "Music/embedded.html",
+		InitFWSize: []int{475, 700},
+		InitEmbSize: []int{360, 240},
+		SupportedExt: []string{".mp3",".flac",".wav",".ogg",".aac",".webm",".mp4"},
+	})
+}
+
+func module_airMusic_getMeta(w http.ResponseWriter, r *http.Request){
+	username, err := authAgent.GetUserName(w,r);
+	if (err != nil){
+		sendErrorResponse(w,"User not logged in")
+		return;
+	}
+
+	playingFile, _ := mv(r, "file", false)
+	playingFile = system_fs_specialURIDecode(playingFile)
+	rPlayingFilePath, _ := virtualPathToRealPath(playingFile, username)
+	fileDir := filepath.ToSlash(filepath.Dir(rPlayingFilePath))
+	supportedFileExt := []string{".mp3",".flac",".wav",".ogg",".aac",".webm",".mp4"}
+	var fileInfos [][]string
+	objs, _ := system_fs_specialGlob(fileDir + "/*")
+	for _, obj := range objs{
+		if (!IsDir(obj) && stringInSlice(filepath.Ext(obj), supportedFileExt)){
+			//This is a file that we want to list
+			var thisFileInfo []string
+			fileExt := filepath.Ext(obj)[1:]
+			fileName := filepath.Base(obj)
+			filePath, _ := realpathToVirtualpath(obj, username)
+			_, hsize, unit, _ := system_fs_getFileSize(obj)
+			size := fmt.Sprintf("%.2f", hsize) + unit;
+
+			thisFileInfo = append(thisFileInfo, fileName)
+			thisFileInfo = append(thisFileInfo, filePath)
+			thisFileInfo = append(thisFileInfo, fileExt)
+			thisFileInfo = append(thisFileInfo, size)
+
+			fileInfos = append(fileInfos, thisFileInfo)
+		}
+	}
+	
+	jsonString, _ := json.Marshal(fileInfos);
+	sendJSONResponse(w, string(jsonString));
+}
+
+func module_airMusic_listSong(w http.ResponseWriter, r *http.Request){
+	username, err :=  authAgent.GetUserName(w,r);
+	if (err != nil){
+		redirectToLoginPage(w,r)
+		return;
+	}
+	
+	var musicDirs []string
+	var playLists []string
+
+	//Initialize user folder structure if it is not yet init
+	uploadDir, _ := virtualPathToRealPath("user:/Music/",username)
+	playList, _ := virtualPathToRealPath("user:/Music/playlist",username)
+	os.MkdirAll(uploadDir, 0755)
+	os.MkdirAll(playList, 0755)
+	musicDirs = append(musicDirs, uploadDir);
+	playLists = append(playLists, playList);
+
+	for _, extStorage := range storages{
+		path := extStorage.Path;
+		if (path[len(path) - 1:] != "/"){
+			path = path + "/"
+		}
+		musicDirs = append(musicDirs, path)
+	}
+
+	//Get which folder the user want to list
+	lsDir, _ := mv(r, "listdir", false)
+	listSong, _ := mv(r, "listSong", false)
+	listFolder, _ := mv(r, "listfolder", false)
+	supportedFileExt := []string{".mp3",".flac",".wav",".ogg",".aac",".webm"}
+
+	//Decode url component if needed
+	if (lsDir != ""){
+		lsDir = strings.ReplaceAll(lsDir, "%2B","+")
+	}
+	
+
+	if (listSong != ""){
+		//List song mode. List the song in the directories
+		if (listSong == "all"){
+			songData := [][]string{}
+			for _, directory := range musicDirs{
+				
+				filepath.Walk(directory,
+					func(path string, info os.FileInfo, err error) error {
+					if err != nil {
+						return err
+					}
+					path = filepath.ToSlash(path)
+					
+					if (stringInSlice(filepath.Ext(path),supportedFileExt)){
+						//This is an audio file. Append to songData
+						var audioFiles []string
+						_, hsize, unit, _ := system_fs_getFileSize(path)
+						size := fmt.Sprintf("%.2f", hsize) + unit;
+						vpath, _ := realpathToVirtualpath(path, username);
+						audioFiles = append(audioFiles, "/media?file=" + vpath);
+						audioFiles = append(audioFiles, strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)));
+						audioFiles = append(audioFiles, filepath.Ext(path)[1:]);
+						audioFiles = append(audioFiles, size);
+						songData = append(songData, audioFiles)
+					}
+					return nil
+				})			
+			}
+
+			jsonString, _ := json.Marshal(songData);
+			sendJSONResponse(w, string(jsonString));
+
+		}else if (strings.Contains(listSong, "search:")){
+			keyword := listSong[7:]
+			songData := [][]string{}
+			for _, directory := range musicDirs{
+				
+				filepath.Walk(directory,
+					func(path string, info os.FileInfo, err error) error {
+					if err != nil {
+						return err
+					}
+					path = filepath.ToSlash(path)
+					
+					if (stringInSlice(filepath.Ext(path),supportedFileExt) && strings.Contains(filepath.Base(path),keyword)){
+						//This is an audio file. Append to songData
+						var audioFiles []string
+						_, hsize, unit, _ := system_fs_getFileSize(path)
+						size := fmt.Sprintf("%.2f", hsize) + unit;
+						vpath, _ := realpathToVirtualpath(path, username);
+						audioFiles = append(audioFiles, "/media?file=" + vpath);
+						audioFiles = append(audioFiles, strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)));
+						audioFiles = append(audioFiles, filepath.Ext(path)[1:]);
+						audioFiles = append(audioFiles, size);
+						songData = append(songData, audioFiles)
+					}
+					return nil
+				})			
+			}
+
+			jsonString, _ := json.Marshal(songData);
+			sendJSONResponse(w, string(jsonString));
+		}else{
+			log.Println("Work in progress")
+		}
+	}else if (lsDir != ""){
+		//List diretory
+		if (lsDir == "root"){
+			var rootInfo [][]string
+			for _, dir := range musicDirs{
+				var thisRootInfo []string
+				//thisRootInfo = append(thisRootInfo, filepath.Base(dir))
+				virtualStorageRootName, err := system_storage_getRootNameByPath(dir, username)
+				if (err != nil){
+					thisRootInfo = append(thisRootInfo, filepath.Base(dir))
+				}else{
+					thisRootInfo = append(thisRootInfo, virtualStorageRootName)
+				}
+				
+				vpath, _ := realpathToVirtualpath(dir,username);
+				thisRootInfo = append(thisRootInfo, vpath + "/")
+				objects, _ := filepath.Glob(dir + "/*")
+				var files []string
+				var folders []string
+				for _, f := range objects{
+					if (IsDir(f)){
+						folders = append(folders, f)
+					}else if (stringInSlice(filepath.Ext(f),supportedFileExt)) {
+						files = append(files, f)
+					}
+				}
+				thisRootInfo = append(thisRootInfo, strconv.Itoa(len(files)))
+				thisRootInfo = append(thisRootInfo, strconv.Itoa(len(folders)))
+				rootInfo = append(rootInfo, thisRootInfo)
+			}
+			jsonString, _ := json.Marshal(rootInfo)
+			sendJSONResponse(w, string(jsonString))
+		}else{
+			listingTarget, _ := virtualPathToRealPath(lsDir, username);
+			if (listingTarget == ""){
+				//User try to leave the accessable area. Reject access.
+				sendErrorResponse(w, "Permission denied")
+				return;
+			}
+			var results [][][]string
+			//List all objects in the current directory and catergorize them
+			folders := []string{}
+			files :=  []string{}
+			//Special glob for handling path with [ or ]
+			objects, _ := system_fs_specialGlob(filepath.Clean(listingTarget) + "/*")
+			for _, obj := range objects{
+				if (IsDir(obj)){
+					folders = append(folders, obj)
+				}else if (stringInSlice(filepath.Ext(obj),supportedFileExt)){
+					files = append(files, obj)
+				}
+			}
+
+			folderInfos := [][]string{}
+			for _, folder := range folders{
+				var thisFolderInfo []string
+				folderName := filepath.Base(folder)
+				folderPath, _ := realpathToVirtualpath(folder, username)
+				filesInDir := 0;
+				DirInDir := 0;
+				objInDir, _ := system_fs_specialGlob(filepath.ToSlash(folder) + "/*")
+				for _, obj := range objInDir{
+					if (IsDir(obj)){
+						DirInDir++;
+					}else if (stringInSlice(filepath.Ext(obj),supportedFileExt)){
+						filesInDir++;
+					}
+				}
+				thisFolderInfo = append(thisFolderInfo, folderName)
+				thisFolderInfo = append(thisFolderInfo, folderPath + "/")
+				thisFolderInfo = append(thisFolderInfo, strconv.Itoa(filesInDir))
+				thisFolderInfo = append(thisFolderInfo, strconv.Itoa(DirInDir))
+				folderInfos = append(folderInfos, thisFolderInfo)
+			}
+
+			fileInfos := [][]string{}
+			for _, file := range files{
+				var thisFileInfo []string
+				vfilepath, _ := realpathToVirtualpath(file, username)
+				filename := filepath.Base(file)
+				ext := filepath.Ext(file)[1:]
+				_, hsize, unit, _ := system_fs_getFileSize(file)
+				size := fmt.Sprintf("%.2f", hsize) + unit;
+
+				thisFileInfo = append(thisFileInfo, "/media?file=" + vfilepath)
+				thisFileInfo = append(thisFileInfo, filename)
+				thisFileInfo = append(thisFileInfo, ext)
+				thisFileInfo = append(thisFileInfo, size)
+				fileInfos = append(fileInfos, thisFileInfo)
+			}
+
+			results = append(results, folderInfos)
+			results = append(results, fileInfos)
+			jsonString, _ := json.Marshal(results)
+			sendJSONResponse(w, string(jsonString))
+			
+		}
+	}else if (listFolder != ""){
+		
+	}
+}
+
+func module_airMusic_getFileInfo(w http.ResponseWriter, r *http.Request){
+	username, err :=  authAgent.GetUserName(w,r);
+	if (err != nil){
+		sendErrorResponse(w, "User not logged in")
+		return;
+	}
+	vpath, _ := mv(r, "filepath", false)
+	
+	//Strip away the access path
+	if (vpath[:12] == "/media?file="){
+		vpath = vpath[12:];
+	}
+
+	//Convert the virtual path to realpath
+	realpath, err := virtualPathToRealPath(vpath, username)
+	if (err != nil){
+		sendErrorResponse(w, "Invalid filepath")
+		return;
+	}
+
+	if (!fileExists(realpath)){
+		sendErrorResponse(w, "File not exists")
+		return;
+	}
+
+	//Buiild the information for sendout
+	results := []string{}
+	results = append(results, filepath.Base(realpath))
+	vdir, _ := realpathToVirtualpath(filepath.Dir(realpath), username)
+	results = append(results, vdir)
+	rawsize, hsize, unit, _ := system_fs_getFileSize(realpath)
+	size := fmt.Sprintf("%.2f", hsize) + unit;
+	results = append(results, size)
+	results = append(results, fmt.Sprintf("%.2f", rawsize))
+	info, err := os.Stat(realpath)
+	results = append(results, info.ModTime().Format("2006-01-02 15:04:05"))
+
+	jsonString, _ := json.Marshal(results)
+	sendJSONResponse(w, string(jsonString))
+}
+

+ 114 - 0
legacy/disabled/module.package.go

@@ -0,0 +1,114 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+)
+
+/*
+	Pacakge management tool for Linux OS with APT
+
+	ONLY USABLE under Linux environment
+*/
+
+func module_package_init() {
+	http.HandleFunc("/system/apt/list", module_package_listAPT)
+}
+
+//Install the given package if not exists. Set mustComply to true for "panic on failed to install"
+func module_package_installIfNotExists(pkgname string, mustComply bool) error {
+	//Clear the pkgname
+	pkgname = strings.ReplaceAll(pkgname, "&", "")
+	pkgname = strings.ReplaceAll(pkgname, "|", "")
+
+	if runtime.GOOS == "windows" {
+		//Check if the command already exists in windows path paramters.
+		cmd := exec.Command("where", pkgname, "2>", "nul")
+		_, err := cmd.CombinedOutput()
+		if err != nil {
+			return errors.New("Package " + pkgname + " not found in Windows %PATH%.")
+		}
+		return nil
+	}
+
+	if *allow_package_autoInstall == false {
+		return errors.New("Package auto install is disabled")
+	}
+
+	cmd := exec.Command("which", pkgname)
+	out, _ := cmd.CombinedOutput()
+
+	if len(string(out)) > 1 {
+		return nil
+	} else {
+		//Package not installed. Install if now if running in sudo mode
+		log.Println("Installing package " + pkgname + "...")
+		cmd := exec.Command("apt-get", "install", "-y", pkgname)
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		err := cmd.Run()
+		if err != nil {
+			if mustComply {
+				//Panic and terminate server process
+				log.Println("Installation failed on package: "+pkgname, string(out))
+				os.Exit(0)
+			} else {
+				log.Println("Installation failed on package: " + pkgname)
+				log.Println(string(out))
+			}
+			return err
+		}
+		return nil
+	}
+
+	return nil
+}
+
+func module_package_test(w http.ResponseWriter, r *http.Request) {
+	module_package_installIfNotExists("ffmpeg", true)
+	module_package_installIfNotExists("samba", true)
+}
+
+func module_package_listAPT(w http.ResponseWriter, r *http.Request) {
+	if runtime.GOOS == "windows" {
+		sendErrorResponse(w, "Function disabled on Windows")
+		return
+	}
+	cmd := exec.Command("apt", "list", "--installed")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	results := [][]string{}
+	//Parse the output string
+	installedPackages := strings.Split(string(out), "\n")
+	for _, thisPackage := range installedPackages {
+		if len(thisPackage) > 0 {
+			packageInfo := strings.Split(thisPackage, "/")
+			packageName := packageInfo[0]
+			if len(packageInfo) >= 2 {
+				packageVersion := strings.Split(packageInfo[1], ",")[1]
+				if packageVersion[:3] == "now" {
+					packageVersion = packageVersion[4:]
+				}
+				if strings.Contains(packageVersion, "[installed") && packageVersion[len(packageVersion)-1:] != "]" {
+					packageVersion = packageVersion + ",automatic]"
+				}
+
+				results = append(results, []string{packageName, packageVersion})
+			}
+		}
+	}
+
+	jsonString, _ := json.Marshal(results)
+	sendJSONResponse(w, string(jsonString))
+	return
+}

+ 161 - 0
legacy/disabled/network.ip2country.go

@@ -0,0 +1,161 @@
+package main
+
+import (
+	"encoding/binary"
+	"encoding/csv"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"strings"
+)
+
+type ipRange struct {
+	start   uint
+	end     uint
+	country string
+}
+
+type ipReturnData struct {
+	IP      string
+	Country string
+	Status  string
+}
+
+func network_ipToCountry_service_init() {
+	log.Println("Starting ip2country service")
+
+	http.HandleFunc("/SystemAO/network/getIPLocation", network_ipToCountry_getCurrentIPLocation)
+	//http.HandleFunc("/SystemAO/network/getPing", network_info_getPing)
+	/*
+		//Register as a system setting
+		registerSetting(settingModule{
+			Name:     "Network Info",
+			Desc:     "System Information",
+			IconPath: "SystemAO/network/img/ethernet.png",
+			Group:    "Network",
+			StartDir: "SystemAO/network/hardware.html",
+		})
+	*/
+}
+
+func network_ipToCountry_getCurrentIPLocation(w http.ResponseWriter, r *http.Request) {
+	/*
+		if system_auth_chkauth(w, r) == false {
+			sendErrorResponse(w, "User not logged in")
+			return
+		}
+	*/
+
+	//Do not try to access IP information if under disable_ip_resolve_services mode
+	if *disable_ip_resolve_services {
+		data := ipReturnData{
+			IP:      "0.0.0.0",
+			Country: "ZZ",
+			Status:  "Resolve Service Disabled",
+		}
+		JSONText, _ := json.Marshal(data)
+		sendJSONResponse(w, string(JSONText))
+		return
+	}
+
+	UserIP, _, err := net.SplitHostPort(string(r.RemoteAddr))
+	if err != nil {
+		log.Println(err)
+	}
+
+	data := ipReturnData{}
+	if strings.Contains(UserIP, ":") {
+		data.IP = UserIP
+		data.Country = "ZZ"
+		data.Status = "IPv6 not supported in current release"
+
+	} else {
+		data.IP = UserIP
+		data.Country = network_ipToCountry_GetCountry(UserIP)
+		data.Status = "OK"
+
+	}
+	JSONText, _ := json.Marshal(data)
+	sendJSONResponse(w, string(JSONText))
+}
+
+//GetCountry returns the country which ip blongs to
+func network_ipToCountry_GetCountry(ip string) string {
+	var arr []ipRange
+	CSVFile := strings.Split(ip, ".")[0]
+	lines, err := network_ipToCountry_ReadCsv("./system/ip2country/" + CSVFile + ".csv")
+	if err != nil {
+		panic(err)
+	}
+
+	// Loop through lines & turn into object
+	for _, line := range lines {
+		StartS := fmt.Sprintf("%s", line[0])
+		Start, _ := network_ipToCountry_ipToInt(StartS)
+		EndS := fmt.Sprintf("%s", line[1])
+		End, _ := network_ipToCountry_ipToInt(EndS)
+
+		data := ipRange{
+			start:   Start,
+			end:     End,
+			country: line[2],
+		}
+		arr = append(arr, data)
+	}
+	ipNumb, err := network_ipToCountry_ipToInt(ip)
+	if err != nil {
+		return ""
+	}
+
+	index := network_ipToCountry_binarySearch(arr, ipNumb, 0, len(arr)-1)
+	if index == -1 {
+		return ""
+	}
+
+	return arr[index].country
+}
+
+func network_ipToCountry_binarySearch(arr []ipRange, hkey uint, low, high int) int {
+	for low <= high {
+		mid := low + (high-low)/2
+		if hkey >= arr[mid].start && hkey <= arr[mid].end {
+			return mid
+		} else if hkey < arr[mid].start {
+			high = mid - 1
+		} else if hkey > arr[mid].end {
+			low = mid + 1
+		}
+	}
+	return -1
+}
+
+func network_ipToCountry_ipToInt(ips string) (uint, error) {
+	ip := net.ParseIP(ips)
+	if len(ip) == 16 {
+		return uint(binary.BigEndian.Uint32(ip[12:16])), nil
+	}
+	return uint(binary.BigEndian.Uint32(ip)), nil
+}
+
+// ReadCsv accepts a file and returns its content as a multi-dimentional type
+// with lines and each column. Only parses to string type.
+func network_ipToCountry_ReadCsv(filename string) ([][]string, error) {
+
+	// Open CSV file
+	f, err := os.Open(filename)
+	if err != nil {
+		return [][]string{}, err
+	}
+	defer f.Close()
+
+	// Read File into a Variable
+	lines, err := csv.NewReader(f).ReadAll()
+	if err != nil {
+		return [][]string{}, err
+	}
+
+	return lines, nil
+}

+ 178 - 0
legacy/disabled/system.boot.go

@@ -0,0 +1,178 @@
+package main
+
+import (
+	"net/http"
+	"encoding/json"
+	"runtime"
+	"strings"
+	"log"
+	"io/ioutil"
+)
+
+/*
+	System Boot Configuration Module
+
+	This module handle the boot flags and settings of the booting paramters
+*/
+
+type bootConfigParamters struct {
+	Hostname                string 	`json:"Hostname"`
+	ListenPort              int 	`json:"ListenPort"`
+	MaxUpload               int 	`json:"MaxUpload"`
+	UploadBuffer            int 	`json:"UploadBuffer"`
+	FileIOBuf               int 	`json:"FileIOBuf"`
+	EnableUPnP				bool 	`json:"EnableUPnP,omitempty"`
+	AllowHardwareMan        bool   	`json:"AllowHardwareMan"`
+	AllowPackageAutoInstall bool   	`json:"AllowPackageAutoInstall"`
+	DisableIPResolve        bool   	`json:"DisableIPResolve"`
+	IsWindows               bool  	`json:"IsWindows,omitempty"`
+}
+
+func system_boot_init(){
+
+	//Register Endpoints
+	http.HandleFunc("/system/boot/generateBootConfig", system_boot_generateBootConfig)
+	http.HandleFunc("/system/boot/getCurrentBootConfig", system_boot_getCurrentBootConfig)
+
+	//Register Settings
+	registerSetting(settingModule{
+		Name:         "Boot Config",
+		Desc:         "Setup Boot Modes and Flags",
+		IconPath:     "SystemAO/boot/img/boot.png",
+		Group:        "Advance",
+		StartDir:     "SystemAO/boot/bootflags.html",
+		RequireAdmin: true,
+	})
+
+}
+
+func system_boot_generateBootConfig(w http.ResponseWriter, r *http.Request) {
+	isAdmin := system_permission_checkUserIsAdmin(w,r);
+	if !isAdmin{
+		sendErrorResponse(w, "Permission Denied")
+		return
+	}
+
+	configs, err := mv(r, "config", true)
+	if err != nil{
+		sendErrorResponse(w, "Internal Server Error")
+		return
+	}
+
+	//Decode config file
+	configs = system_fs_specialURIDecode(configs)
+
+	//Generate the boot script from the given paramters
+	parsedConfig := new(bootConfigParamters);
+	err = json.Unmarshal([]byte(configs), &parsedConfig)
+	if err != nil{
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	log.Println("Warning! Boot configuration updated!")
+
+	commandSlice := []string{
+		"-hostname", 
+		"\"" + parsedConfig.Hostname + "\"",
+		"-port",
+		IntToString(parsedConfig.ListenPort),
+		"-max_upload_size",
+		IntToString(parsedConfig.MaxUpload),
+		"-upload_buf",
+		IntToString(parsedConfig.UploadBuffer),
+		"-iobuf",
+		IntToString(parsedConfig.FileIOBuf),
+	}
+
+	if !parsedConfig.AllowHardwareMan{
+		commandSlice = append(commandSlice, "-enable_hwman=false")
+	}
+
+	if !parsedConfig.AllowPackageAutoInstall{
+		commandSlice = append(commandSlice, "-allow_pkg_install=false")
+	}
+
+	if parsedConfig.DisableIPResolve{
+		commandSlice = append(commandSlice, "-disable_ip_resolver=true")
+	}
+		
+
+	execpath := strings.Join(commandSlice, " ")
+
+	//Replace all odd things like && and || and & etc
+	if (strings.Contains(execpath, "&&") || strings.Contains(execpath, "||") || strings.Contains(execpath, "&") || strings.Contains(execpath, ">")){
+		sendErrorResponse(w, "Configuration contains invalid characters")
+		return
+	}
+
+	//Generate the corrisponding scrip file
+	binaryPath := ""
+	scriptName := "start.sh"
+	if runtime.GOOS == "windows" {
+		binaryPath = "aroz_online_windows_amd64.exe"
+		scriptName = "start.bat"
+	} else if runtime.GOOS == "linux" {
+		if runtime.GOARCH == "arm" {
+			binaryPath = "aroz_online_linux_arm"
+		}else if runtime.GOARCH == "arm64" {
+			binaryPath = "aroz_online_linux_arm64"
+		}else if runtime.GOARCH == "386" {
+			binaryPath = "aroz_online_windows_386"
+		}else if runtime.GOARCH == "amd64" {
+			binaryPath = "aroz_online_linux_amd64"
+		}
+	}
+
+	//Build the final start script
+	execpath =  binaryPath + " " + execpath
+	if runtime.GOOS == "windows" {
+		err = ioutil.WriteFile(scriptName, []byte(execpath), 0755)
+		if err != nil{
+			sendErrorResponse(w, err.Error())
+			return
+		}
+	}else{
+		//Append other services to the script
+		scriptContent := "#/bin/bash\nsudo " + execpath
+		err = ioutil.WriteFile(scriptName, []byte(scriptContent), 0755)
+		if err != nil{
+			sendErrorResponse(w, err.Error())
+			return
+		}
+	}
+
+	sendOK(w)
+	
+}
+
+//Get the current booting flags (Only those for basic users)
+func system_boot_getCurrentBootConfig(w http.ResponseWriter, r *http.Request){
+	isAdmin := system_permission_checkUserIsAdmin(w,r);
+	if !isAdmin{
+		sendErrorResponse(w, "Permission Denied")
+		return
+	}
+
+	isWindows := false;
+	if runtime.GOOS == "windows" {
+        isWindows = true
+    }
+
+	//Create a struct for the current booting options
+	jsonString, _ := json.Marshal(bootConfigParamters{
+		ListenPort: *listen_port,
+		Hostname: *host_name,
+		EnableUPnP: *allow_upnp,
+		MaxUpload: *max_upload,
+		UploadBuffer: *upload_buf,
+		FileIOBuf: *file_opr_buff,
+		AllowHardwareMan: *allow_hardware_management,
+		AllowPackageAutoInstall: *allow_package_autoInstall,
+		DisableIPResolve: *disable_ip_resolve_services,
+		IsWindows: isWindows,
+	})
+
+	sendJSONResponse(w, string(jsonString))
+
+}

+ 302 - 0
legacy/disabled/system.disk.quota.go

@@ -0,0 +1,302 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"log"
+)
+
+/*
+	Disk Quota Management System
+	This module manage the user groups disk quota in the system
+
+	Disk quota can only be set on a user group bases.
+	(aka all users in the same group has the identical number of disk quota to the group settings)
+*/
+
+func system_disk_quota_init() {
+	//Initiate quota storage table
+	err := sysdb.NewTable("diskquota")
+	if err != nil {
+		panic(err)
+	}
+
+	//Register Endpoints
+	http.HandleFunc("/system/disk/quota/setQuota", system_disk_quota_setQuota)
+	http.HandleFunc("/system/disk/quota/listQuota", system_disk_quota_listQuota)
+	http.HandleFunc("/system/disk/quota/quotaInfo", system_disk_quota_handleQuotaInfo)
+	http.HandleFunc("/system/disk/quota/quotaDist", system_disk_quota_handleFileDistributionView)
+
+	//Register Setting Interfaces
+	//Register interface fow viewing the user storage quota
+	registerSetting(settingModule{
+		Name:     "Storage Quota",
+		Desc:     "User Remaining Space",
+		IconPath: "SystemAO/disk/quota/img/small_icon.png",
+		Group:    "Disk",
+		StartDir: "SystemAO/disk/quota/quota.system",
+	})
+
+	//Register interface for admin to setup quota settings
+	registerSetting(settingModule{
+		Name:         "Quota Settings",
+		Desc:         "Setup Group Storage Limit",
+		IconPath:     "SystemAO/disk/quota/img/small_icon.png",
+		Group:        "Disk",
+		StartDir:     "SystemAO/disk/quota/manage.html",
+		RequireAdmin: true,
+	})
+
+}
+
+//Get a list of quota on user groups and their storage limit
+func system_disk_quota_listQuota(w http.ResponseWriter, r *http.Request) {
+	_, err := authAgent.GetUserName(w,r);
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+	isAdmin := system_permission_checkUserIsAdmin(w, r)
+	if !isAdmin {
+		sendErrorResponse(w, "Permission denied")
+		return
+	}
+
+	groups := system_permission_listGroup()
+	results := map[string]int64{}
+	for _, group := range groups {
+		quota, _ := system_disk_quota_getQuotaFromGroupname(group)
+		results[group] = quota
+	}
+
+	jsonString, _ := json.Marshal(results)
+	sendJSONResponse(w, string(jsonString))
+}
+
+//Check the storage quota on this usergroup. Return -1 for unlimited quota and 0 if error
+func system_disk_quota_getQuotaFromGroupname(groupname string) (int64, error) {
+	//If administrator, always return -1
+	if groupname == "administrator" {
+		return -1, nil
+	}
+	//Check if group exists
+	if !system_permission_groupExists(groupname) {
+		return 0, errors.New("Group not exists")
+	}
+
+	//Group exists. Get the group quota from db
+	groupQuota := int64(0)
+	err := sysdb.Read("diskquota", "quota/"+groupname, &groupQuota)
+	if err != nil {
+		return 0, err
+	}
+	return groupQuota, nil
+}
+
+//Check if the given size can fit into the user remaining quota, return true if the file fit user quota
+func system_disk_quota_validateQuota(username string, filesize int64) bool {
+	remaining, _, _, err := system_disk_quota_quotaInfo(username)
+	if err != nil{
+		log.Println("Upload failed for user: " + username + " " + err.Error())
+		return false
+	}
+	//log.Println(remaining, filesize, err)
+	if remaining == -1{
+		//Unlimited quota. Always return true
+		return true
+	}else if (remaining == 0){
+		//Read only account. Always return false
+		return false
+	}else if (remaining >= filesize ){
+		//This file fits in the user's remaining space
+		return true
+	}else{
+		return false
+	}
+	return false
+}
+
+//Check if the given path apply quota limitation
+func system_disk_quota_checkIfQuotaApply(path string, username string) bool{
+	targetStoargeDevice, err := system_storage_getStorageByPath(path, username);
+	if (err != nil){
+		return false
+	}
+	if targetStoargeDevice.Hierarchy == "user"{
+		//User Hierarchy Storage Device, count as user's private storage
+		return true
+	}
+	//Not user's private storage. Calculate as public one
+	return false
+}
+
+//Set the storage quota of the particular user
+func system_disk_quota_setQuota(w http.ResponseWriter, r *http.Request) {
+	authed := authAgent.CheckAuth(r)
+	if !authed {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+	isAdmin := system_permission_checkUserIsAdmin(w, r)
+	if !isAdmin {
+		sendErrorResponse(w, "Permission denied")
+		return
+	}
+
+	//OK to proceed
+	groupname, err := mv(r, "groupname", true)
+	if err != nil {
+		sendErrorResponse(w, "Group name not defned")
+		return
+	}
+
+	quotaSizeString, err := mv(r, "quota", true)
+	if err != nil {
+		sendErrorResponse(w, "Quota not defined")
+		return
+	}
+
+	quotaSize, err := StringToInt64(quotaSizeString)
+	if err != nil || quotaSize < 0 {
+		sendErrorResponse(w, "Invalid quota size given")
+		return
+	}
+	//Qutasize unit is in MB
+	quotaSize = quotaSize << 20
+
+	//Check groupname exists
+	if !system_permission_groupExists(groupname) {
+		sendErrorResponse(w, "Group name not exists. Given "+groupname)
+		return
+	}
+
+	//Ok to proceed.
+	err = sysdb.Write("diskquota", "quota/"+groupname, quotaSize)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	sendOK(w)
+}
+
+//Show the current user's quota information
+func system_disk_quota_handleQuotaInfo(w http.ResponseWriter, r *http.Request) {
+	username, err := authAgent.GetUserName(w,r);
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	remainingSpace, usedSpace, totalSpace, err := system_disk_quota_quotaInfo(username)
+	type quotaInformation struct {
+		Remaining int64
+		Used      int64
+		Total     int64
+	}
+
+	jsonString, _ := json.Marshal(quotaInformation{
+		Remaining: remainingSpace,
+		Used:      usedSpace,
+		Total:     totalSpace,
+	})
+
+	sendJSONResponse(w, string(jsonString))
+
+}
+
+//Get all the users file and see how
+func system_disk_quota_handleFileDistributionView(w http.ResponseWriter, r *http.Request) {
+	//Check if the user logged in
+	username, err := authAgent.GetUserName(w,r);
+	if err != nil {
+		sendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	//Create a file distribution list
+	fileDist := map[string]int64{}
+	userpaths := system_storage_getUserDirectory(username)
+	for _, thispath := range userpaths {
+		filepath.Walk(thispath, func(filepath string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if !info.IsDir() {
+				mime, _, err := system_fs_getMime(filepath)
+				if err != nil {
+					return err
+				}
+				mediaType := strings.SplitN(mime, "/", 2)[0]
+				mediaType = strings.Title(mediaType)
+				fileDist[mediaType] = fileDist[mediaType] + info.Size()
+			}
+			return err
+		})
+	}
+
+	//Sort the file according to the number of files in the
+	type kv struct {
+		Mime string
+		Size int64
+	}
+
+	var ss []kv
+	for k, v := range fileDist {
+		ss = append(ss, kv{k, v})
+	}
+
+	sort.Slice(ss, func(i, j int) bool {
+		return ss[i].Size > ss[j].Size
+	})
+
+	//Return the distrubution using json string
+	jsonString, _ := json.Marshal(ss)
+	sendJSONResponse(w, string(jsonString))
+}
+
+//Get the quota information of the current user. Return the followings
+/*
+	Remaining space of the user quota (int64)
+	Used space of the user quota (int64)
+	Total theoretical space of the user quota (int64)
+	Error (error). Standard error message if something goes wrong
+*/
+func system_disk_quota_quotaInfo(username string) (int64, int64, int64, error) {
+	//Get the user group information
+	usergroup := system_permission_getUserPermissionGroup(username)
+	groupExists := system_permission_groupExists(usergroup)
+	if !groupExists {
+		return 0, 0, 0, errors.New("User group not exists")
+	}
+
+	//Get the group quota information
+	groupQuota := int64(-1)
+	sysdb.Read("diskquota", "quota/"+usergroup, &groupQuota)
+
+	//Calculate user limit
+	userpaths := system_storage_getUserDirectory(username)
+	totalUserUsedSpace := int64(0)
+	for _, thispath := range userpaths {
+		filepath.Walk(thispath, func(_ string, info os.FileInfo, err error) error {
+			if err != nil {
+				return err
+			}
+			if !info.IsDir() {
+				totalUserUsedSpace += info.Size()
+			}
+			return err
+		})
+	}
+
+	remainingSpace := groupQuota - totalUserUsedSpace
+	if groupQuota == -1 {
+		remainingSpace = -1
+	}
+	return remainingSpace, totalUserUsedSpace, groupQuota, nil
+}

+ 381 - 0
legacy/disabled/system.disk.smart.go

@@ -0,0 +1,381 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"net/http"
+	"os/exec"
+	"runtime"
+	"time"
+)
+
+//SystemSmartExecutable xxx
+var SystemSmartExecutable = ""
+
+//SMARTInformation xxx
+var SMARTInformation = []SMART{}
+var lastScanTime int64 = 0
+
+// DevicesList was used for storing the disk scanning result
+type DevicesList struct {
+	JSONFormatVersion []int `json:"json_format_version"`
+	Smartctl          struct {
+		Version      []int    `json:"version"`
+		SvnRevision  string   `json:"svn_revision"`
+		PlatformInfo string   `json:"platform_info"`
+		BuildInfo    string   `json:"build_info"`
+		Argv         []string `json:"argv"`
+		Messages     []struct {
+			String   string `json:"string"`
+			Severity string `json:"severity"`
+		} `json:"messages"`
+		ExitStatus int `json:"exit_status"`
+	} `json:"smartctl"`
+	Devices []struct {
+		Name     string `json:"name"`
+		InfoName string `json:"info_name"`
+		Type     string `json:"type"`
+		Protocol string `json:"protocol"`
+	} `json:"devices"`
+}
+
+// DeviceSMART was used for storing each disk smart information
+type DeviceSMART struct {
+	JSONFormatVersion []int `json:"json_format_version"`
+	Smartctl          struct {
+		Version      []int    `json:"version"`
+		SvnRevision  string   `json:"svn_revision"`
+		PlatformInfo string   `json:"platform_info"`
+		BuildInfo    string   `json:"build_info"`
+		Argv         []string `json:"argv"`
+		Messages     []struct {
+			String   string `json:"string"`
+			Severity string `json:"severity"`
+		} `json:"messages"`
+		ExitStatus int `json:"exit_status"`
+	} `json:"smartctl"`
+	Device struct {
+		Name     string `json:"name"`
+		InfoName string `json:"info_name"`
+		Type     string `json:"type"`
+		Protocol string `json:"protocol"`
+	} `json:"device"`
+	ModelFamily  string `json:"model_family"`
+	ModelName    string `json:"model_name"`
+	SerialNumber string `json:"serial_number"`
+	Wwn          struct {
+		Naa int   `json:"naa"`
+		Oui int   `json:"oui"`
+		ID  int64 `json:"id"`
+	} `json:"wwn"`
+	FirmwareVersion string `json:"firmware_version"`
+	UserCapacity    struct {
+		Blocks int   `json:"blocks"`
+		Bytes  int64 `json:"bytes"`
+	} `json:"user_capacity"`
+	LogicalBlockSize   int  `json:"logical_block_size"`
+	PhysicalBlockSize  int  `json:"physical_block_size"`
+	RotationRate       int  `json:"rotation_rate"`
+	InSmartctlDatabase bool `json:"in_smartctl_database"`
+	AtaVersion         struct {
+		String     string `json:"string"`
+		MajorValue int    `json:"major_value"`
+		MinorValue int    `json:"minor_value"`
+	} `json:"ata_version"`
+	SataVersion struct {
+		String string `json:"string"`
+		Value  int    `json:"value"`
+	} `json:"sata_version"`
+	InterfaceSpeed struct {
+		Max struct {
+			SataValue      int    `json:"sata_value"`
+			String         string `json:"string"`
+			UnitsPerSecond int    `json:"units_per_second"`
+			BitsPerUnit    int    `json:"bits_per_unit"`
+		} `json:"max"`
+		Current struct {
+			SataValue      int    `json:"sata_value"`
+			String         string `json:"string"`
+			UnitsPerSecond int    `json:"units_per_second"`
+			BitsPerUnit    int    `json:"bits_per_unit"`
+		} `json:"current"`
+	} `json:"interface_speed"`
+	LocalTime struct {
+		TimeT   int    `json:"time_t"`
+		Asctime string `json:"asctime"`
+	} `json:"local_time"`
+	SmartStatus struct {
+		Passed bool `json:"passed"`
+	} `json:"smart_status"`
+	AtaSmartData struct {
+		OfflineDataCollection struct {
+			Status struct {
+				Value  int    `json:"value"`
+				String string `json:"string"`
+			} `json:"status"`
+			CompletionSeconds int `json:"completion_seconds"`
+		} `json:"offline_data_collection"`
+		SelfTest struct {
+			Status struct {
+				Value  int    `json:"value"`
+				String string `json:"string"`
+				Passed bool   `json:"passed"`
+			} `json:"status"`
+			PollingMinutes struct {
+				Short      int `json:"short"`
+				Extended   int `json:"extended"`
+				Conveyance int `json:"conveyance"`
+			} `json:"polling_minutes"`
+		} `json:"self_test"`
+		Capabilities struct {
+			Values                        []int `json:"values"`
+			ExecOfflineImmediateSupported bool  `json:"exec_offline_immediate_supported"`
+			OfflineIsAbortedUponNewCmd    bool  `json:"offline_is_aborted_upon_new_cmd"`
+			OfflineSurfaceScanSupported   bool  `json:"offline_surface_scan_supported"`
+			SelfTestsSupported            bool  `json:"self_tests_supported"`
+			ConveyanceSelfTestSupported   bool  `json:"conveyance_self_test_supported"`
+			SelectiveSelfTestSupported    bool  `json:"selective_self_test_supported"`
+			AttributeAutosaveEnabled      bool  `json:"attribute_autosave_enabled"`
+			ErrorLoggingSupported         bool  `json:"error_logging_supported"`
+			GpLoggingSupported            bool  `json:"gp_logging_supported"`
+		} `json:"capabilities"`
+	} `json:"ata_smart_data"`
+	AtaSctCapabilities struct {
+		Value                         int  `json:"value"`
+		ErrorRecoveryControlSupported bool `json:"error_recovery_control_supported"`
+		FeatureControlSupported       bool `json:"feature_control_supported"`
+		DataTableSupported            bool `json:"data_table_supported"`
+	} `json:"ata_sct_capabilities"`
+	AtaSmartAttributes struct {
+		Revision int `json:"revision"`
+		Table    []struct {
+			ID         int    `json:"id"`
+			Name       string `json:"name"`
+			Value      int    `json:"value"`
+			Worst      int    `json:"worst"`
+			Thresh     int    `json:"thresh"`
+			WhenFailed string `json:"when_failed"`
+			Flags      struct {
+				Value         int    `json:"value"`
+				String        string `json:"string"`
+				Prefailure    bool   `json:"prefailure"`
+				UpdatedOnline bool   `json:"updated_online"`
+				Performance   bool   `json:"performance"`
+				ErrorRate     bool   `json:"error_rate"`
+				EventCount    bool   `json:"event_count"`
+				AutoKeep      bool   `json:"auto_keep"`
+			} `json:"flags"`
+			Raw struct {
+				Value  int    `json:"value"`
+				String string `json:"string"`
+			} `json:"raw"`
+		} `json:"table"`
+	} `json:"ata_smart_attributes"`
+	PowerOnTime struct {
+		Hours   int `json:"hours"`
+		Minutes int `json:"minutes"`
+	} `json:"power_on_time"`
+	PowerCycleCount int `json:"power_cycle_count"`
+	Temperature     struct {
+		Current int `json:"current"`
+	} `json:"temperature"`
+	AtaSmartSelfTestLog struct {
+		Standard struct {
+			Revision int `json:"revision"`
+			Table    []struct {
+				Type struct {
+					Value  int    `json:"value"`
+					String string `json:"string"`
+				} `json:"type"`
+				Status struct {
+					Value  int    `json:"value"`
+					String string `json:"string"`
+					Passed bool   `json:"passed"`
+				} `json:"status,omitempty"`
+				LifetimeHours int `json:"lifetime_hours"`
+			} `json:"table"`
+			Count              int `json:"count"`
+			ErrorCountTotal    int `json:"error_count_total"`
+			ErrorCountOutdated int `json:"error_count_outdated"`
+		} `json:"standard"`
+	} `json:"ata_smart_self_test_log"`
+	AtaSmartSelectiveSelfTestLog struct {
+		Revision int `json:"revision"`
+		Table    []struct {
+			LbaMin int `json:"lba_min"`
+			LbaMax int `json:"lba_max"`
+			Status struct {
+				Value  int    `json:"value"`
+				String string `json:"string"`
+			} `json:"status"`
+		} `json:"table"`
+		Flags struct {
+			Value                int  `json:"value"`
+			RemainderScanEnabled bool `json:"remainder_scan_enabled"`
+		} `json:"flags"`
+		PowerUpScanResumeMinutes int `json:"power_up_scan_resume_minutes"`
+	} `json:"ata_smart_selective_self_test_log"`
+}
+
+// SMART was used for storing all Devices data
+type SMART struct {
+	Port       string       `json:"Port"`
+	DriveSmart *DeviceSMART `json:"SMART"`
+}
+
+// DiskSmartInit Desktop script initiation
+func system_disk_smart_init() {
+	log.Println("Starting SMART mointoring")
+	if !(fileExists("system/disk/smart/win/smartctl.exe") || fileExists("system/disk/smart/linux/smartctl_arm") || fileExists("system/disk/smart/linux/smartctl_arm64") || fileExists("system/disk/smart/linux/smartctl_i386")) {
+		if build_version == "development" {
+			log.Fatal("[SMART Mointoring] One or more binary not found.")
+		} else {
+			panic("[SMART Mointoring] One or more binary not found.")
+		}
+
+	}
+	if runtime.GOOS == "windows" {
+		SystemSmartExecutable = "./system/disk/smart/win/smartctl.exe"
+	} else if runtime.GOOS == "linux" {
+		if runtime.GOARCH == "arm" {
+			SystemSmartExecutable = "./system/disk/smart/linux/smartctl_armv6"
+		}
+		if runtime.GOARCH == "arm64" {
+			SystemSmartExecutable = "./system/disk/smart/linux/smartctl_armv6"
+		}
+		if runtime.GOARCH == "386" {
+			SystemSmartExecutable = "./system/disk/smart/linux/smartctl_i386"
+		}
+		if runtime.GOARCH == "amd64" {
+			SystemSmartExecutable = "./system/disk/smart/linux/smartctl_i386"
+		}
+	} else {
+		if build_version == "development" {
+			//log.Fatal("[SMART Mointoring] This webApp can't run on imcompitiable environment")
+		} else {
+			panic("[SMART Mointoring] This webApp can't run on imcompitiable environment")
+		}
+
+	}
+	//Register all the required API
+	http.HandleFunc("/system/disk/smart/getSMART", GetSMART)
+	http.HandleFunc("/system/disk/smart/getSMARTTable", checkDiskTable)
+	http.HandleFunc("/system/disk/smart/getLogInfo", checkDiskTestStatus)
+
+	//Only allow SMART under sudo moude
+	if sudo_mode {
+		//Register as a system setting
+		registerSetting(settingModule{
+			Name:         "Disk SMART",
+			Desc:         "HardDisk Health Checking",
+			IconPath:     "SystemAO/disk/smart/img/small_icon.png",
+			Group:        "Disk",
+			StartDir:     "SystemAO/disk/smart/smart.html",
+			RequireAdmin: true,
+		})
+
+		registerSetting(settingModule{
+			Name:         "SMART Log",
+			Desc:         "HardDisk Health Log",
+			IconPath:     "SystemAO/disk/smart/img/small_icon.png",
+			Group:        "Disk",
+			StartDir:     "SystemAO/disk/smart/log.html",
+			RequireAdmin: true,
+		})
+	}
+
+}
+
+// ReadSMART xxx
+func ReadSMART() []SMART {
+	if time.Now().Unix()-lastScanTime > 30 {
+		SMARTInformation = []SMART{}
+		//Scan disk
+		cmd := exec.Command(SystemSmartExecutable, "--scan", "--json=c")
+		out, _ := cmd.CombinedOutput()
+		Devices := new(DevicesList)
+		DevicesOutput := string(out)
+		json.Unmarshal([]byte(DevicesOutput), &Devices)
+		for _, element := range Devices.Devices {
+			//Load SMART for each drive
+			cmd := exec.Command(SystemSmartExecutable, "-i", element.Name, "-a", "--json=c")
+			out, _ = cmd.CombinedOutput()
+			InvSMARTInformation := new(DeviceSMART)
+			SMARTOutput := string(out)
+			json.Unmarshal([]byte(SMARTOutput), &InvSMARTInformation)
+			if len(InvSMARTInformation.Smartctl.Messages) > 0 {
+				if InvSMARTInformation.Smartctl.Messages[0].Severity == "error" {
+					log.Println("[SMART Mointoring] Disk " + element.Name + " cannot be readed")
+				} else {
+					//putting everything into that struct array
+					n := SMART{Port: element.Name, DriveSmart: InvSMARTInformation}
+					SMARTInformation = append(SMARTInformation, n)
+				}
+			} else {
+				//putting everything into that struct array
+				n := SMART{Port: element.Name, DriveSmart: InvSMARTInformation}
+				SMARTInformation = append(SMARTInformation, n)
+			}
+
+		}
+		lastScanTime = time.Now().Unix()
+	}
+	return SMARTInformation
+}
+
+func GetSMART(w http.ResponseWriter, r *http.Request) {
+	//Check if user has logged in
+	if authAgent.CheckAuth(r) == false {
+		redirectToLoginPage(w, r)
+		return
+	}
+	jsonText, _ := json.Marshal(ReadSMART())
+	//send!
+	sendJSONResponse(w, string(jsonText))
+}
+
+func checkDiskTable(w http.ResponseWriter, r *http.Request) {
+	//Check if user has logged in
+	if authAgent.CheckAuth(r) == false {
+		redirectToLoginPage(w, r)
+		return
+	}
+	disks, ok := r.URL.Query()["disk"]
+	if !ok || len(disks[0]) < 1 {
+		log.Println("Parameter DISK not found.")
+		return
+	}
+
+	DiskStatus := new(DeviceSMART)
+	for _, info := range ReadSMART() {
+		if info.Port == disks[0] {
+			DiskStatus = info.DriveSmart
+		}
+	}
+	JSONStr, _ := json.Marshal(DiskStatus.AtaSmartAttributes.Table)
+	//send!
+	sendJSONResponse(w, string(JSONStr))
+}
+
+func checkDiskTestStatus(w http.ResponseWriter, r *http.Request) {
+	//Check if user has logged in
+	if authAgent.CheckAuth(r) == false {
+		redirectToLoginPage(w, r)
+		return
+	}
+	disks, ok := r.URL.Query()["disk"]
+	if !ok || len(disks[0]) < 1 {
+		log.Println("Parameter DISK not found.")
+		return
+	}
+
+	DiskTestStatus := new(DeviceSMART)
+	for _, info := range ReadSMART() {
+		if info.Port == disks[0] {
+			DiskTestStatus = info.DriveSmart
+		}
+	}
+	JSONStr, _ := json.Marshal(DiskTestStatus.AtaSmartData.SelfTest.Status)
+	//send!
+	sendJSONResponse(w, string(JSONStr))
+}

+ 150 - 0
legacy/disabled/system.disk.space.go

@@ -0,0 +1,150 @@
+package main
+
+import (
+	"net/http"
+	"os/exec"
+	"strings"
+	"encoding/json"
+	"log"
+	"runtime"
+)
+
+/*
+	Disk Space Services
+
+	Return the disk information of the system. 
+	The most basic task of all
+*/
+
+type LogicalDiskSpaceInfo struct{
+	Device string
+	Volume  int64
+	Used   int64
+	Available int64
+	UsedPercentage string
+	MountPoint string
+}
+
+func system_disk_space_init(){
+	//Register API
+	http.HandleFunc("/system/disk/space/list", system_disk_space_handleList)
+
+	//Register settings
+	registerSetting(settingModule{
+		Name:     "Disk Space",
+		Desc:     "System Storage Space on Disks",
+		IconPath: "SystemAO/disk/space/img/small_icon.png",
+		Group:    "Disk",
+		StartDir: "SystemAO/disk/space/diskspace.html",
+	})
+}
+
+func system_disk_space_handleList(w http.ResponseWriter, r *http.Request){
+	allDisksVolume := system_disk_space_getDriveInfo();
+	jsonString, _ := json.Marshal(allDisksVolume);
+	sendJSONResponse(w, string(jsonString))
+}
+
+func system_disk_space_getDriveInfo() []LogicalDiskSpaceInfo{
+	if runtime.GOOS == "windows" {
+		//Check window disk info, wip
+		cmd := exec.Command("wmic", "logicaldisk","get","caption,size,freespace")
+		out, err := cmd.CombinedOutput()
+		if (err != nil){
+			log.Println("wmic not supported.")
+			return []LogicalDiskSpaceInfo{};
+		}
+		lines := strings.Split(string(out),"\n")
+		var results []LogicalDiskSpaceInfo
+		for _,line := range lines{
+			if (strings.Contains(line, ":")){
+				//This is a valid drive
+
+				line = strings.TrimSpace(line)
+				//Tidy the line
+				for strings.Contains(line, "  "){
+					line = strings.Replace(line, "  "," ",-1)
+				}
+
+				//Split by space
+				infoChunk := strings.Split(line, " ")
+				if len(infoChunk) == 1{
+					//Drive reserved and not mounted, like SD card adapters
+					results = append(results, LogicalDiskSpaceInfo{
+						Device: infoChunk[0],
+						Volume: 0,
+						Used: 	0,
+						Available: 0,
+						UsedPercentage: "Not Mounted",
+						MountPoint: infoChunk[0],
+					})
+				}else if (len(infoChunk) > 2){
+					size, err := StringToInt64(infoChunk[2])
+					if (err != nil){
+						size = 0;
+					}
+					freespace, err := StringToInt64(infoChunk[1])
+					if (err != nil){
+						size = 0;
+					}
+					usedSpace := size - freespace;
+					percentage := int64(float64(usedSpace) / float64(size) * 100)
+
+					results = append(results, LogicalDiskSpaceInfo{
+						Device: infoChunk[0],
+						Volume: size,
+						Used: 	usedSpace,
+						Available: freespace,
+						UsedPercentage: IntToString(int(percentage)) + "%",
+						MountPoint: infoChunk[0],
+					})
+				}
+			}
+		}
+
+		return results
+	}else{
+		//Get drive status using df command
+		cmdin := `df -k | sed -e /Filesystem/d`
+		cmd := exec.Command("bash","-c",cmdin)
+		dev, err := cmd.CombinedOutput()
+		if err != nil {
+			dev = []byte{}
+		}
+
+		drives := strings.Split(string(dev), "\n")
+
+		if (len(drives) == 0){
+			return []LogicalDiskSpaceInfo{}
+		}
+
+		var arr []LogicalDiskSpaceInfo
+		for _, driveInfo := range drives {
+			if (driveInfo == ""){
+				continue;
+			}
+			for strings.Contains(driveInfo, "  "){
+				driveInfo = strings.Replace(driveInfo, "  ", " ", -1)
+			}
+			driveInfoChunk := strings.Split(driveInfo, " ")
+			volume, _ := StringToInt64(driveInfoChunk[1])
+			usedSpace, _ := StringToInt64(driveInfoChunk[2])
+			freespaceInByte, _ := StringToInt64(driveInfoChunk[3])
+			
+			LogicalDisk := LogicalDiskSpaceInfo{
+				Device: driveInfoChunk[0],
+				Volume: volume * 1024,
+				Used: 	usedSpace * 1024,
+				Available: freespaceInByte * 1024,
+				UsedPercentage: driveInfoChunk[4],
+				MountPoint: driveInfoChunk[5],
+			}
+			arr = append(arr, LogicalDisk)
+		}
+
+		return arr
+	}
+	
+	return []LogicalDiskSpaceInfo{}
+}
+

+ 264 - 0
legacy/disabled/system.permission.go

@@ -0,0 +1,264 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"strings"
+	//"log"
+)
+
+/*
+	This is the permission management module of the ArOZ Online System
+
+	In default mode, the system only contains a user group named "administrator"
+	This module also handle permission checking and others
+*/
+
+//Initiate function for system permission
+func system_permission_service_init() {
+	//Register permission configuration endpoints
+	http.HandleFunc("/system/permission/listgroup", system_permission_handleListGroup)
+	http.HandleFunc("/system/permission/newgroup", system_permission_createUserGroupHandler)
+	http.HandleFunc("/system/permission/delgroup", system_permission_removeUserGroupHandler)
+	http.HandleFunc("/system/permission/isAdmin", system_permission_handleAdminCheck)
+	//http.HandleFunc("/system/permission/groupdetails", system_permission_handleGroupDetail)
+
+	//Create table if not exists
+	sysdb.NewTable("permission")
+
+	//Register setting interface for module configuration
+	registerSetting(settingModule{
+		Name:         "Permission Groups",
+		Desc:         "Handle the permission of access in groups",
+		IconPath:     "SystemAO/users/img/small_icon.png",
+		Group:        "Users",
+		StartDir:     "SystemAO/users/group.html",
+		RequireAdmin: true,
+	})
+
+}
+
+func system_permission_getUserGroups(username string) (string, error) {
+	group := ""
+	sysdb.Read( "auth", "group/"+username, &group)
+	if group == "" {
+		return "", errors.New("User group not found")
+	}
+	return group, nil
+}
+
+//Return a list of usergorup stored in the system
+func system_permission_listGroup() []string {
+	groups := []string{}
+	entries, _ := sysdb.ListTable("permission")
+	for _, keypairs := range entries {
+		if strings.Contains(string(keypairs[0]), "group/") {
+			groups = append(groups, strings.Split(string(keypairs[0]), "/")[1])
+		}
+	}
+
+	return groups
+}
+
+//Return the current list of usergroup from the database as JSON
+func system_permission_handleListGroup(w http.ResponseWriter, r *http.Request) {
+	groups := []string{}
+	entries, _ := sysdb.ListTable("permission")
+	listPermission, _ := mv(r, "showper", false)
+	if listPermission == "" {
+		for _, keypairs := range entries {
+			if strings.Contains(string(keypairs[0]), "group/") {
+				//This is a group name record. Append the group name only.
+				groups = append(groups, strings.Split(string(keypairs[0]), "/")[1])
+			}
+		}
+
+		//Check if the group list is empty. If yes, create the administrator group
+		if len(groups) == 0 {
+			err := system_permission_createGroup("administrator", []string{"*"})
+			if err != nil {
+				panic("Failed to create administrator group. Is database writable?")
+			}
+			groups = append(groups, "administrator")
+		}
+
+		jsonString, _ := json.Marshal(groups)
+		sendJSONResponse(w, string(jsonString))
+	} else {
+		results := map[string][]string{}
+		for _, keypairs := range entries {
+			if strings.Contains(string(keypairs[0]), "group/") {
+				//This is a group name record. Append the group name only.
+				thisGroupPermission := []string{}
+				json.Unmarshal(keypairs[1], &thisGroupPermission)
+				results[strings.Split(string(keypairs[0]), "/")[1]] = thisGroupPermission
+			}
+		}
+
+		jsonString, _ := json.Marshal(results)
+		sendJSONResponse(w, string(jsonString))
+	}
+
+	/*
+		//Deprecated method for listing group with JSON string storage
+		groupsData := "";
+		sysdb.Read( "permission", "groups", &groups)
+		//There are always at least one group and this key must be valid. Not need to check for error.
+		w.Header().Set("Content-Type", "application/json")
+		sendTextResponse(w,groups)
+	*/
+}
+
+func system_permission_handleAdminCheck(w http.ResponseWriter, r *http.Request) {
+	isAdmin := system_permission_checkUserIsAdmin(w, r)
+	if isAdmin {
+		sendJSONResponse(w, "true")
+	} else {
+		sendJSONResponse(w, "false")
+	}
+}
+
+func system_permission_handleGroupDetail(w http.ResponseWriter, r *http.Request) {
+	opr, _ := mv(r, "opr", false)
+	if opr == "" {
+		//List all groups with detail
+		var groupsRaw []byte
+		sysdb.Read( "permission", "groups", &groupsRaw)
+		var groups []string
+		json.Unmarshal(groupsRaw, &groups)
+
+	}
+}
+
+func system_permission_createGroup(groupname string, modulepermission []string) error {
+	//Check if group already exists
+	if !system_permission_groupExists(groupname) {
+		//This group do not exists. Continue to create
+		err := sysdb.Write("permission", "group/"+groupname, modulepermission)
+		if err != nil {
+			return err
+		}
+	} else {
+		//This group exists.
+		return errors.New("Group already exists")
+	}
+	return nil
+}
+
+func system_permission_removeUserGroupHandler(w http.ResponseWriter, r *http.Request) {
+	//Check if user is admin
+	isAdmin := system_permission_checkUserIsAdmin(w, r)
+	if !isAdmin {
+		sendErrorResponse(w, "Permission denied")
+		return
+	}
+
+	//Check if the groupname is provided
+	groupname, err := mv(r, "groupname", false)
+	if err != nil {
+		sendErrorResponse(w, "Groupname not defined")
+		return
+	}
+
+	//Remove the group from database
+	if system_permission_groupExists(groupname) {
+		//This group exits. Continue removal
+		err := sysdb.Delete("permission", "group/"+groupname)
+		if err != nil {
+			sendErrorResponse(w, err.Error())
+			return
+		}
+	} else {
+		//This group exists.
+		sendErrorResponse(w, "Given group not exists")
+		return
+	}
+
+	sendOK(w)
+}
+
+func system_permission_createUserGroupHandler(w http.ResponseWriter, r *http.Request) {
+	isAdmin := system_permission_checkUserIsAdmin(w, r)
+	if !isAdmin {
+		sendErrorResponse(w, "Permission denied")
+		return
+	}
+	groupname, err := mv(r, "groupname", true)
+	if err != nil {
+		sendErrorResponse(w, "Groupname not defined")
+		return
+	}
+
+	permissions, err := mv(r, "permission", true)
+	if err != nil {
+		sendErrorResponse(w, "Permission not defined")
+		return
+	}
+	permissionList := []string{}
+	err = json.Unmarshal([]byte(permissions), &permissionList)
+	if err != nil {
+		sendErrorResponse(w, "Failed to parse the permission list")
+		return
+	}
+	system_permission_createGroup(groupname, permissionList)
+	sendOK(w)
+}
+
+
+func system_permission_getGroupAccessList(groupname string) []string {
+	moduleList := []string{}
+	err := sysdb.Read( "permission", "group/"+groupname, &moduleList)
+	if err != nil {
+		return []string{}
+	}
+	return moduleList
+}
+
+func system_permission_checkUserHasAccessToModule(username string, modulename string) bool {
+	//Get user group and see if group exists.
+	usergroup := system_permission_getUserPermissionGroup(username)
+	groupExists := system_permission_groupExists(usergroup)
+	if !groupExists {
+		return false
+	}
+
+	//Group exists. Check permission on module
+	groupAccessList := system_permission_getGroupAccessList(usergroup)
+	if len(groupAccessList) == 1 && groupAccessList[0] == "*" {
+		return true
+	}
+	if stringInSlice(modulename, groupAccessList) {
+		return true
+	}
+
+	return false
+}
+
+func system_permission_checkUserIsAdmin(w http.ResponseWriter, r *http.Request) bool {
+	username, err := authAgent.GetUserName(w, r)
+	if err != nil{
+		return false
+	}
+	userGroup := system_permission_getUserPermissionGroup(username)
+	if userGroup == "administrator" {
+		return true
+	}
+	return false
+}
+
+func system_permission_getUserPermissionGroup(username string) string {
+	usergroup := ""
+	sysdb.Read( "auth", "group/"+username, &usergroup)
+	return (usergroup)
+}
+
+//This function check if the given usergroup ID exists.
+func system_permission_groupExists(group string) bool {
+	dummyPermission := []string{}
+	err := sysdb.Read( "permission", "group/"+group, &dummyPermission)
+	if err != nil || len(dummyPermission) == 0 {
+		return false
+	}
+	return true
+}

+ 114 - 0
legacy/disabled/system.power.go

@@ -0,0 +1,114 @@
+package main
+
+import (
+	"log"
+	"net/http"
+	"os/exec"
+	"runtime"
+)
+
+/*
+	Power Management Module
+
+	This module will handle the power condition of the system, including poweroff and restart
+*/
+
+func system_power_init() {
+	http.HandleFunc("/system/power/shutdown", system_power_poweroff)
+	http.HandleFunc("/system/power/restart", system_power_restart)
+}
+
+func system_power_poweroff(w http.ResponseWriter, r *http.Request) {
+	isAdmin := system_permission_checkUserIsAdmin(w, r)
+	if !isAdmin {
+		sendErrorResponse(w, "Permission Denied")
+		return
+	}
+
+	if !sudo_mode {
+		sendErrorResponse(w, "Sudo mode required")
+		return
+	}
+
+	if runtime.GOOS == "windows" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("shutdown", "-s", "-t", "20")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "linux" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("/sbin/shutdown")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "darwin" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("sudo", "shutdown", "-h", "+1")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	sendOK(w)
+}
+
+func system_power_restart(w http.ResponseWriter, r *http.Request) {
+	isAdmin := system_permission_checkUserIsAdmin(w, r)
+	if !isAdmin {
+		sendErrorResponse(w, "Permission Denied")
+		return
+	}
+
+	if !sudo_mode {
+		sendErrorResponse(w, "Sudo mode required")
+		return
+	}
+
+	if runtime.GOOS == "windows" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("shutdown", "-r", "-t", "20")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "linux" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("systemctl", "reboot")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+
+	if runtime.GOOS == "darwin" {
+		//Only allow Linux to do power operation
+		cmd := exec.Command("shutdown", "-r", "+1")
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			log.Println(string(out))
+			sendErrorResponse(w, string(out))
+		}
+		log.Println(string(out))
+	}
+	sendOK(w)
+}

+ 150 - 0
legacy/disabled/system.resetpw.go

@@ -0,0 +1,150 @@
+package main
+
+import (
+	"net/http"
+	"log"
+	"errors"
+
+	auth "imuslab.com/arozos/mod/auth"
+)
+
+/*
+	Password Reset Module
+
+	This module exists to serve the password restart page with security check
+*/
+
+func system_resetpw_init(){
+	http.HandleFunc("/system/reset/validateResetKey", system_resetpw_validateResetKeyHandler);
+	http.HandleFunc("/system/reset/confirmPasswordReset", system_resetpw_confirmReset);
+}
+
+//Validate if the ysername and rkey is valid
+func system_resetpw_validateResetKeyHandler(w http.ResponseWriter, r *http.Request){
+	username, err := mv(r, "username", true)
+	if err != nil{
+		sendErrorResponse(w, "Internal Server Error")
+		return
+	}
+	rkey, err := mv(r, "rkey", true)
+	if err != nil{
+		sendErrorResponse(w, "Internal Server Error")
+		return
+	}
+
+	if username == "" || rkey == "" {
+		sendErrorResponse(w, "Invalid username or rkey")
+		return
+	}
+
+	//Check if the pair is valid
+	err = system_resetpw_validateResetKey(username, rkey)
+	if err != nil{
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	sendOK(w)
+
+}
+
+func system_resetpw_confirmReset(w http.ResponseWriter, r *http.Request){
+	username, _ := mv(r, "username", true)
+	rkey, _ := mv(r, "rkey", true)
+	newpw, _ := mv(r, "pw", true)
+	if (username == "" || rkey == "" || newpw == ""){
+		sendErrorResponse(w, "Internal Server Error")
+		return
+	}
+
+	//Check user exists
+	if !authAgent.UserExists(username){
+		sendErrorResponse(w, "Username not exists")
+		return
+	}
+
+	//Validate rkey
+	err := system_resetpw_validateResetKey(username, rkey)
+	if err != nil{
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	//OK to procced
+	newHashedPassword := auth.Hash(newpw)
+	err = sysdb.Write("auth", "passhash/" + username, newHashedPassword)
+	if err != nil{
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	sendOK(w);
+
+}
+
+func system_resetpw_validateResetKey(username string, key string) error{
+	//Get current password from db
+	passwordInDB := ""
+	err := sysdb.Read("auth", "passhash/" + username, &passwordInDB)
+	if err != nil{
+		return err
+	}
+
+	//Get hashed user key
+	hashedKey := auth.Hash(key)
+	if (passwordInDB != hashedKey){
+		return errors.New("Invalid Password Reset Key")
+	}
+
+	return nil
+}
+
+func system_resetpw_handlePasswordReset(w http.ResponseWriter, r *http.Request){
+	//Check if the user click on this link with reset password key string. If not, ask the user to input one
+	acc, err := mv(r, "acc", false)
+	if err != nil || acc == "" {
+		system_resetpw_serveIdEnterInterface(w,r);
+		return
+	}
+
+	resetkey, err := mv(r, "rkey", false)
+	if err != nil || resetkey == "" {
+		system_resetpw_serveIdEnterInterface(w,r);
+		return
+	}
+
+	//Check if the code is valid
+	err = system_resetpw_validateResetKey(acc, resetkey)
+	if err != nil {
+		sendErrorResponse(w, "Invalid username or resetKey")
+		return
+	}
+
+	//OK. Create the New Password Entering UI
+	imageBase64, _ := LoadImageAsBase64("./web/" + iconVendor)
+	template, err := template_load("system/reset/resetPasswordTemplate.html",map[string]interface{}{
+		"vendor_logo": imageBase64,
+		"host_name": *host_name,
+		"username": acc,
+		"rkey": resetkey,
+	});
+	if err != nil{
+		log.Fatal(err);
+	}
+	w.Header().Set("Content-Type", "text/html; charset=UTF-8")
+	w.Write([]byte(template))
+}
+
+func system_resetpw_serveIdEnterInterface(w http.ResponseWriter, r *http.Request){
+	//Reset Key or Username not found, Serve entering interface
+	imageBase64, _ := LoadImageAsBase64("./web/" + iconVendor)
+	template, err := template_load("system/reset/resetCodeTemplate.html",map[string]interface{}{
+		"vendor_logo": imageBase64,
+		"host_name": *host_name,
+	});
+	if err != nil{
+		log.Fatal(err);
+	}
+	w.Header().Set("Content-Type", "text/html; charset=UTF-8")
+	w.Write([]byte(template))
+}

+ 237 - 0
legacy/disabled/system.users.go

@@ -0,0 +1,237 @@
+package main
+
+import (
+	"net/http"
+	"log"
+	"strings"
+	"encoding/json"
+	"github.com/satori/go.uuid"
+
+	auth "imuslab.com/arozos/mod/auth"
+)
+
+/*
+	USERS MANAGER
+
+	Manage user creation, listing, remove and others
+*/
+
+func system_user_init(){
+	http.HandleFunc("/system/users/list", system_user_handleList)
+	http.HandleFunc("/system/users/editUser", system_user_handleUserEdit)
+	http.HandleFunc("/system/users/userinfo", system_user_handleUserInfo)
+
+	//Register setting interface for module configuration
+	registerSetting(settingModule{
+		Name: "My Account",
+		Desc: "Manage your account and password",
+		IconPath: "SystemAO/users/img/small_icon.png",
+		Group: "Users",
+		StartDir: "SystemAO/users/account.html",
+		RequireAdmin: false,
+	})
+
+	registerSetting(settingModule{
+		Name: "User List",
+		Desc: "A list of users registered on this system",
+		IconPath: "SystemAO/users/img/small_icon.png",
+		Group: "Users",
+		StartDir: "SystemAO/users/userList.html",
+		RequireAdmin: true,
+	})
+}
+
+//User edit handle. For admin to change settings for a user
+func system_user_handleUserEdit(w http.ResponseWriter, r *http.Request){
+	//Require admin access
+	if !system_permission_checkUserIsAdmin(w,r){
+        sendErrorResponse(w, "Permission denied")
+	}
+	
+	opr, _ := mv(r, "opr", true)
+	username, _ := mv(r, "username", true)
+	if !system_user_userExists(username){
+		sendErrorResponse(w, "User not exists")
+		return
+	}
+
+	if opr == ""{
+		//List this user information
+		type returnValue struct{
+			Username string;
+			Icondata string;
+			Usergroup string;
+		}
+		iconData := getUserIcon(username)
+		userGroup, err := system_permission_getUserGroups(username)
+		if (err != nil){
+			sendErrorResponse(w, "Unable to get user group")
+			return;
+		}
+		jsonString, _ := json.Marshal(returnValue{
+			Username: username,
+			Icondata: iconData,
+			Usergroup: userGroup,
+		})
+
+		sendJSONResponse(w, string(jsonString))
+	}else if opr == "updateUserGroup"{
+		//Update the target user's group
+		newgroup, err := mv(r, "newgroup", true)
+		if err != nil{
+			sendErrorResponse(w, "New Group not defined");
+			return
+		}
+
+		//Check if new group exists
+		if !system_permission_groupExists(newgroup){
+			sendErrorResponse(w, "Group not exists")
+			return
+		}
+
+		//OK to proceed
+		err = sysdb.Write("auth", "group/" + username, newgroup)
+		if err != nil{
+			sendErrorResponse(w, err.Error())
+			return
+		}
+		sendOK(w)
+	}else if opr == "resetPassword"{
+		//Reset password for this user
+		//Generate a random password for this user
+		tmppassword := uuid.NewV4().String()
+		hashedPassword := auth.Hash(tmppassword);
+    	err := sysdb.Write("auth", "passhash/" + username, hashedPassword)
+		if err != nil{
+			sendErrorResponse(w, err.Error())
+			return
+		}
+		//Finish. Send back the reseted password
+		sendJSONResponse(w, "\"" + tmppassword + "\"")
+
+	}else{
+		sendErrorResponse(w, "Not supported opr")
+		return
+	}
+
+	
+}
+
+//User Info handler. Handle user's editing for his / her own profile
+func system_user_handleUserInfo(w http.ResponseWriter, r *http.Request){
+	username, err := authAgent.GetUserName(w,r);
+	if (err != nil){
+		sendErrorResponse(w, "User not logged in")
+		return;
+	}
+	opr, _ := mv(r, "opr", true)
+
+	if (opr == ""){
+		//Listing mode
+		iconData := getUserIcon(username)
+		userGroup, err := system_permission_getUserGroups(username)
+		if (err != nil){
+			sendErrorResponse(w, "Unable to get user group")
+			return;
+		}
+		type returnValue struct{
+			Username string;
+			Icondata string;
+			Usergroup string;
+		}
+		jsonString, _ := json.Marshal(returnValue{
+			Username: username,
+			Icondata: iconData,
+			Usergroup: userGroup,
+		})
+
+		sendJSONResponse(w, string(jsonString))
+		return;
+	}else if (opr == "changepw"){
+		oldpw, _ := mv(r, "oldpw", true)
+		newpw, _ := mv(r, "newpw", true)
+		if (oldpw == "" || newpw == ""){
+			sendErrorResponse(w, "Password cannot be empty")
+			return;
+		}
+		//valid the old password
+		hashedPassword := auth.Hash(oldpw)
+		var passwordInDB string
+		err = sysdb.Read("auth", "passhash/" + username, &passwordInDB)
+		if (hashedPassword != passwordInDB){
+			//Old password entry invalid.
+			sendErrorResponse(w, "Invalid old password.")
+			return;
+		}
+		//OK! Change user password
+		newHashedPassword := auth.Hash(newpw)
+		sysdb.Write("auth", "passhash/" + username, newHashedPassword)
+		sendOK(w);
+	}else if (opr == "changeprofilepic"){
+		picdata, _ := mv(r, "picdata", true)
+		if (picdata != ""){
+			setUserIcon(username, picdata);
+			sendOK(w);
+		}else{
+			sendErrorResponse(w, "Empty image data received.")
+			return
+		}
+	}else{
+		sendErrorResponse(w, "Not supported opr")
+		return
+	}
+}
+
+//Get and set user profile icon
+func getUserIcon(username string) string{
+	var userIconpath []byte;
+	sysdb.Read("auth","profilepic/" + username, &userIconpath)
+	return string(userIconpath);
+}
+
+func setUserIcon(username string, base64data string){
+	sysdb.Write("auth","profilepic/" + username, []byte(base64data))
+	return
+}
+
+func system_user_userExists(username string) bool{
+	//Implement alternative interface for checking user exists
+	return authAgent.UserExists(username);
+}
+
+func system_user_handleList(w http.ResponseWriter, r *http.Request){
+	//List all users within the auth database.
+	if (authAgent.CheckAuth(r) == false){
+        //This user has not logged in
+        sendErrorResponse(w, "User not logged in");
+        return;
+    }
+	if (system_permission_checkUserIsAdmin(w,r) == true){
+		entries,_ := sysdb.ListTable("auth")
+		results := [][]string{}
+		for _, keypairs := range entries{
+			if (strings.Contains(string(keypairs[0]), "group/")){
+				username:= strings.Split(string(keypairs[0]),"/")[1]
+				group := ""
+				//Get user icon if it exists in the database
+				userIcon := getUserIcon(username)
+				
+				//Get the user account states
+				accountStatus := "normal"
+				sysdb.Read("auth","acstatus/" + username, &accountStatus)
+
+				json.Unmarshal(keypairs[1], &group)
+				results = append(results, []string{username, group, userIcon, accountStatus})
+			}
+		}
+		
+		jsonString, _ := json.Marshal(results)
+		sendJSONResponse(w, string(jsonString))
+		return
+	}else{
+		username, _ := authAgent.GetUserName(w,r);
+		log.Println("[Permission] " + username + " tries to access admin only function.")
+		sendErrorResponse(w, "Permission denied")
+		return;
+	}
+}

+ 330 - 0
legacy/module.Photo.go_disabled

@@ -0,0 +1,330 @@
+package main
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"image"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"path/filepath"
+
+	"strings"
+
+	"github.com/disintegration/imaging"
+
+	reflect "reflect"
+)
+
+/*
+	Photo - The worst Photo Viewer for ArOZ Online
+
+	 By Alan Yeung, 2020
+
+*/
+
+//SupportFileExt shouldn't be exported
+var SupportFileExt = []string{".jpg", ".jpeg", ".gif", ".tiff", ".png", ".tif", ".heif"}
+
+//Output shouldn't be exported.
+type Output struct {
+	URL      string
+	Filename string
+	Size     string
+	CacheURL string
+	Height   int
+	Width    int
+}
+
+//OutputFolder shouldn't be exported
+type OutputFolder struct {
+	VPath      string
+	Foldername string
+}
+
+//SearchReturn shouldn't be exported
+type SearchReturn struct {
+	Success bool `json:"success"`
+	Results []struct {
+		Name     string `json:"name"`
+		Value    string `json:"value"`
+		Text     string `json:"text"`
+		Disabled bool   `json:"disabled,omitempty"`
+	} `json:"results"`
+}
+
+func module_Photo_init() {
+	http.HandleFunc("/Photo/listPhoto", ModulePhotoListPhoto)
+	http.HandleFunc("/Photo/listFolder", ModulePhotoListFolder)
+	http.HandleFunc("/Photo/search", ModulePhotoSearch)
+	//http.HandleFunc("/Photo/getMeta", module_Photo_getMeta)
+
+	//Register this module to system
+	registerModule(moduleInfo{
+		Name:         "Photo",
+		Desc:         "The Photo Viewer for ArOZ Online",
+		Group:        "Media",
+		IconPath:     "Photo/img/module_icon.png",
+		Version:      "0.0.1",
+		StartDir:     "Photo/index.html",
+		SupportFW:    true,
+		LaunchFWDir:  "Photo/index.html",
+		SupportEmb:   true,
+		LaunchEmb:    "Photo/embedded.html",
+		InitFWSize:   []int{800, 600},
+		InitEmbSize:  []int{800, 600},
+		SupportedExt: SupportFileExt,
+	})
+
+}
+
+//ModulePhotoListPhoto shouldn't be exported
+func ModulePhotoListPhoto(w http.ResponseWriter, r *http.Request) {
+	username, err := system_auth_getUserName(w, r)
+	if err != nil {
+		redirectToLoginPage(w, r)
+		return
+	}
+
+	//obtain folder name from GET request
+	folder, ok := r.URL.Query()["folder"]
+
+	//check if GET request exists, if not then using default path
+	if !ok || len(folder[0]) < 1 {
+		folder = append(folder, "user:/Photo/Photo/uploads")
+	}
+
+	//obtain filter from GET request
+	filter, ok := r.URL.Query()["q"]
+
+	//check if GET request exists, if not then using null
+	if !ok || len(folder[0]) < 1 {
+		filter = append(filter, "")
+	}
+
+	Alldata, _ := QueryDir(folder[0], filter[0], username)
+	jsonString, _ := json.Marshal(Alldata)
+	sendJSONResponse(w, string(jsonString))
+}
+
+//ModulePhotoListFolder shouldn't be exported
+func ModulePhotoListFolder(w http.ResponseWriter, r *http.Request) {
+	username, err := system_auth_getUserName(w, r)
+	if err != nil {
+		redirectToLoginPage(w, r)
+		return
+	}
+
+	storageDir, _ := virtualPathToRealPath("user:/Photo/Photo/storage", username)
+	os.MkdirAll(storageDir, 0755)
+
+	Alldata := []OutputFolder{}
+
+	filepath.Walk(storageDir, func(path string, info os.FileInfo, e error) error {
+		if e != nil {
+			return e
+		}
+
+		if info.Mode().IsDir() && path != storageDir {
+			vPath, _ := realpathToVirtualpath(path, username)
+
+			folderData := OutputFolder{
+				VPath:      vPath,
+				Foldername: filepath.Base(path),
+			}
+			Alldata = append(Alldata, folderData)
+		}
+		return nil
+	})
+
+	jsonString, _ := json.Marshal(Alldata)
+	sendJSONResponse(w, string(jsonString))
+}
+
+//QueryDir shouldn't be exported.
+func QueryDir(vPath string, filter string, username string) ([]Output, error) {
+	//create dir
+	uploadDir, _ := virtualPathToRealPath(vPath, username)
+	cacheDir, _ := virtualPathToRealPath("user:/Photo/Photo/thumbnails", username)
+	os.MkdirAll(uploadDir, 0755)
+	os.MkdirAll(cacheDir, 0755)
+
+	Alldata := []Output{}
+	files, _ := filepath.Glob(uploadDir + "/*")
+	for _, file := range files {
+		if stringInSlice(filepath.Ext(file), SupportFileExt) {
+			if chkFilter(file, filter) {
+				//File path (vpath)
+				vpath, _ := realpathToVirtualpath(file, username)
+
+				//File Size
+				_, hsize, unit, _ := system_fs_getFileSize(file)
+				size := fmt.Sprintf("%.2f", hsize) + unit
+
+				//File cache location
+				cacheFilename, _ := resizePhoto(file, username)
+				cacheFilevPath := "user:/Photo/Photo/thumbnails/" + cacheFilename
+
+				//Get image width height
+				cacheFilePhyPath, _ := virtualPathToRealPath(cacheFilevPath, username)
+				width, height := getImageDimension(cacheFilePhyPath)
+
+				fileData := Output{
+					URL:      vpath,
+					Filename: filepath.Base(file),
+					Size:     size,
+					CacheURL: cacheFilevPath,
+					Height:   height,
+					Width:    width,
+				}
+				Alldata = append(Alldata, fileData)
+			}
+		}
+	}
+	return Alldata, nil
+}
+
+//ModulePhotoSearch shouldn't be exported
+func ModulePhotoSearch(w http.ResponseWriter, r *http.Request) {
+	username, err := system_auth_getUserName(w, r)
+	if err != nil {
+		redirectToLoginPage(w, r)
+		return
+	}
+	_ = username
+
+	//obtain folder name from GET request
+	queryString, ok := r.URL.Query()["q"]
+
+	QResult := new(SearchReturn)
+	QResult.Success = true
+
+	//check if GET request exists, if not then using default val
+	if !ok || len(queryString[0]) < 1 {
+		QResult.Success = false
+	} else {
+		n := struct {
+			Name     string `json:"name"`
+			Value    string `json:"value"`
+			Text     string `json:"text"`
+			Disabled bool   `json:"disabled,omitempty"`
+		}{Name: "file:" + queryString[0], Value: "file:" + queryString[0], Text: "file:" + queryString[0], Disabled: false}
+		QResult.Results = append(QResult.Results, n)
+	}
+
+	jsonString, _ := json.Marshal(QResult)
+	sendJSONResponse(w, string(jsonString))
+}
+
+func resizePhoto(filename string, username string) (string, error) {
+	cacheDir, _ := virtualPathToRealPath("user:/Photo/Photo/thumbnails", username)
+	//Generate hash for that file
+	md5, err := hashFilemd5(filename)
+	if err != nil {
+		return "", err
+	}
+	//check if file exist, if true then return
+	if fileExists(cacheDir + "/" + md5 + ".jpg") {
+		return md5 + ".jpg", nil
+	}
+
+	// Open image.
+	src, _ := imaging.Open(filename)
+	// Resize the cropped image to width = 200px preserving the aspect ratio.
+	src = imaging.Resize(src, 200, 0, imaging.Lanczos)
+	// Save the resulting image as JPEG.
+	err = imaging.Save(src, cacheDir+"/"+md5+".jpg")
+	if err != nil {
+		log.Fatalf("failed to save image: %v", err)
+	}
+
+	return md5 + ".jpg", nil
+}
+
+//hashFilemd5 copy from https://mrwaggel.be/post/generate-md5-hash-of-a-file-in-golang/
+func hashFilemd5(filePath string) (string, error) {
+	var returnMD5String string
+	file, err := os.Open(filePath)
+	if err != nil {
+		return returnMD5String, err
+	}
+	defer file.Close()
+	hash := md5.New()
+	if _, err := io.Copy(hash, file); err != nil {
+		return returnMD5String, err
+	}
+	hashInBytes := hash.Sum(nil)[:16]
+	returnMD5String = hex.EncodeToString(hashInBytes)
+	return returnMD5String, nil
+
+}
+
+//thanks https://gist.github.com/sergiotapia/7882944
+func getImageDimension(imagePath string) (int, int) {
+	file, err := os.Open(imagePath)
+	if err != nil {
+		//log.Fprintf(os.Stderr, "%v\n", err)
+	}
+
+	image, _, err := image.DecodeConfig(file)
+	if err != nil {
+		//log.Fprintf(os.Stderr, "%s: %v\n", imagePath, err)
+	}
+	return image.Width, image.Height
+}
+
+var funcs = map[string]interface{}{"file": file}
+
+func chkFilter(imagePath string, filter string) bool {
+	if filter == "" {
+		return true
+	}
+	filtersli := strings.Split(filter, ",")
+	Filterbool := make(map[string]bool)
+
+	if len(filtersli) > 0 {
+		log.Println(filtersli)
+		for _, item := range filtersli {
+			itemArr := strings.Split(item, ":") // [0] = func name , [1] = value
+			log.Println(item)
+			returnResult, _ := Call(funcs, itemArr[0], itemArr[1], imagePath, filepath.Base(imagePath))
+			Filterbool[item] = Filterbool[item] || returnResult[0].Bool()
+		}
+	}
+
+	returnBool := true
+	if len(Filterbool) > 0 {
+		for _, item := range Filterbool {
+			returnBool = returnBool && item
+		}
+	}
+	return returnBool
+}
+
+//https://mikespook.com/2012/07/function-call-by-name-in-golang/
+//Call shouldn not be exported
+func Call(m map[string]interface{}, name string, params ...interface{}) (result []reflect.Value, err error) {
+	f := reflect.ValueOf(m[name])
+	if len(params) != f.Type().NumIn() {
+		err = errors.New("The number of params is not adapted.")
+		return
+	}
+	in := make([]reflect.Value, len(params))
+	for k, param := range params {
+		in[k] = reflect.ValueOf(param)
+	}
+	result = f.Call(in)
+	return
+}
+
+func file(queryString, filename string, filePath string) bool {
+	if strings.Contains(filename, queryString) {
+		return true
+	} else {
+		return false
+	}
+}

+ 138 - 0
legacy/module.Video.go_disabled

@@ -0,0 +1,138 @@
+package main
+
+import (
+	"net/http"
+	"path/filepath"
+	"encoding/json"
+	"log"
+)
+
+func module_Video_init(){
+	http.HandleFunc("/Video/buildPlaylist", module_ArdPlayer_buildPlaylist)
+
+	//Register module
+	registerModule(moduleInfo{
+		Name: "Video",
+		Desc: "The basic video player for ArOZ Online",
+		Group: "Media",
+		IconPath: "Video/img/module_icon.png",
+		Version: "0.0.4",
+		StartDir: "Video/index.html",
+		SupportFW: true,
+		LaunchFWDir: "Video/index.html",
+		SupportEmb: true,
+		LaunchEmb: "Video/embedded.html",
+		InitFWSize: []int{585, 820},
+		InitEmbSize: []int{700, 470},
+		SupportedExt: []string{".webm",".mp4",".ogg"},
+	})
+}
+
+//Scan all the attached storage devices and generate a global playlist
+func module_ArdPlayer_buildPlaylist(w http.ResponseWriter, r *http.Request){
+	username, err := system_auth_getUserName(w,r);
+	if (err != nil){
+		sendErrorResponse(w,"User not logged in")
+		return;
+	}
+	
+	type videoFile struct{
+		Filename string
+		Filepath string
+		Ext string
+	}
+
+	type playList struct{
+		Name string
+		Files []videoFile
+	}
+
+	type viewPoint struct{
+		StorageName string
+		PlayLists []playList
+		UnsortedVideos []videoFile
+	}
+
+	results := []viewPoint{}
+	for _, dev := range storages{
+		//Get the base dir of this storage device
+		scanBaseDir := ""
+		devicePath := dev.Path;
+		if (devicePath[len(devicePath)-1:] != "/"){
+			devicePath = devicePath + "/"
+		}
+		if (dev.Hierarchy == "users"){
+			scanBaseDir = devicePath + username + "/Video"
+		}else if (dev.Hierarchy == "public"){
+			scanBaseDir = devicePath
+		}
+
+		if (scanBaseDir == "" || !fileExists(scanBaseDir)){
+			//This directory has no supported hierarchy or root folder not exists
+			continue;
+		}
+		//log.Println(scanBaseDir)
+
+		//Scan this directory for folders or video files and build a playlist out of it
+		
+		supportExt := []string{".mp4",".webm",".ogg"}
+		objs, _ := system_fs_specialGlob(scanBaseDir)
+		//Declare a new ViewPort for this device
+		thisViewPort := new(viewPoint)
+		allPlayLists := []playList{}
+		unsortedFiles := []videoFile{}
+		for _, file := range objs{
+			if (IsDir(file)){
+				//This is a playlist. List all its contents
+				filesInside := []string{}
+				filesInPlaylist, _ := system_fs_specialGlob(file + "/")
+				for _, videoInList := range filesInPlaylist{
+					if (system_fs_matchFileExt(videoInList, supportExt)){
+						filesInside = append(filesInside, videoInList)
+					}
+				}
+				if (len(filesInside) > 0){
+					//This is a valid playlist
+					thisPlayList := new(playList)
+					thisPlayList.Name = filepath.Base(file)
+					videosInPlaylist := []videoFile{}
+					for _, videoInPlaylist := range filesInside{
+						thisVideoFile := new(videoFile)
+						thisVideoFile.Filename = filepath.Base(videoInPlaylist)
+						thisVideoFile.Filepath, _ = realpathToVirtualpath(videoInPlaylist, username)
+						thisVideoFile.Ext = filepath.Ext(videoInPlaylist)
+						videosInPlaylist = append(videosInPlaylist, *thisVideoFile)
+					}
+					thisPlayList.Files = videosInPlaylist;
+					allPlayLists = append(allPlayLists, *thisPlayList)
+				}
+			}else if (system_fs_matchFileExt(file, supportExt)){
+				//This is an unsorted video file
+				vpath, _ := realpathToVirtualpath(file, username)
+				thisVideoFile := &videoFile{
+					Filename: filepath.Base(file),
+					Filepath: vpath,
+					Ext: filepath.Ext(file),
+				}
+				unsortedFiles = append(unsortedFiles, *thisVideoFile)
+			}
+		}
+
+		//Build the required objects from information
+		thisViewPort.PlayLists = allPlayLists
+		thisViewPort.UnsortedVideos = unsortedFiles
+		thisViewPort.StorageName = dev.Name
+		results = append(results,*thisViewPort)
+	}
+
+	//Format the results as JSON string and output
+	jsonString, err := json.Marshal(results);
+	if (err != nil){
+		log.Println("[ArdPlayer] Unable to parse playlist")
+		sendErrorResponse(w, "Unable to parse playlist.")
+		return
+	}
+	sendJSONResponse(w, string(jsonString))
+	return;
+
+}

+ 25 - 0
legacy/module.dummy.go_disabled

@@ -0,0 +1,25 @@
+package main
+
+import (
+
+)
+
+/*
+	DUMMY MODULE
+
+	This is a module for testing API functionality. 
+	DO NOT COMPILE THIS MODULE INTO PRODUCTION
+*/
+
+func module_dummy_init(){
+
+	//Register the module
+	registerModule(moduleInfo{
+		Name: "Dummy",
+		Group: "Development",
+		IconPath: "Dummy/img/small_icon.png",
+		Version: "0.1",
+		StartDir: "Dummy/index.html",
+	})
+}
+

+ 63 - 0
legacy/module.notepadA.go_disabled

@@ -0,0 +1,63 @@
+package main
+
+import (
+	"net/http"
+	"encoding/json"
+)
+
+func module_notepadA_init(){
+	http.HandleFunc("/NotepadA/store", module_notepadA_handleStorage)
+
+	//Create database for this module
+	system_db_newTable(sysdb, "NotepadA")
+
+	//Register this module to system
+	registerModule(moduleInfo{
+		Name: "NotepadA",
+		Desc: "The best code editor on ArOZ Online",
+		Group: "Office",
+		IconPath: "NotepadA/img/module_icon.png",
+		Version: "1.2",
+		StartDir: "NotepadA/index.html",
+		SupportFW: true,
+		LaunchFWDir: "NotepadA/index.html",
+		SupportEmb: true,
+		LaunchEmb: "NotepadA/embedded.html",
+		InitFWSize: []int{1024, 768},
+		InitEmbSize: []int{360, 200},
+		SupportedExt: []string{".bat",".coffee",".cpp",".cs",".csp",".csv",".fs",".dockerfile",".go",".html",".ini",".java",".js",".lua",".mips",".md", ".sql",".txt",".php",".py",".ts",".xml",".yaml"},
+	})
+}
+
+func module_notepadA_handleStorage(w http.ResponseWriter, r *http.Request){
+	username, err := system_auth_getUserName(w,r);
+	if (err != nil){
+		sendErrorResponse(w,"User not logged in")
+		return;
+	}
+	opr, _ := mv(r, "opr", true)
+	key, _ := mv(r, "key", true)
+	value, _ := mv(r, "value", true)
+
+	userKey := username + "/" + key;
+	if (opr == "get"){
+		returnString := "";
+		err := system_db_read(sysdb, "NotepadA", userKey, &returnString)
+		if (err != nil){
+			sendErrorResponse(w, err.Error())
+			return
+		}
+		jsonstring, _ := json.Marshal(returnString)
+		sendJSONResponse(w, string(jsonstring));
+		return
+	}else if (opr == "set"){
+		err := system_db_write(sysdb, "NotepadA", userKey, value)
+		if (err != nil){
+			sendErrorResponse(w, err.Error())
+			return
+		}
+		sendOK(w);
+	}
+	
+}
+

+ 18 - 0
localhost.crt

@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC8DCCAdigAwIBAgIUHxbG5YxgI7VtgPsP4re1yQgBs+IwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMDUwOTExMjExMloXDTIwMDYw
+ODExMjExMlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEAyebTyiqhDKMRd4G+KOQyMoq4NVAx657/tEFx1kLsDY7j
+Pc0zfdjdAbzVGBRcZwbIgtpfOXSWnh91TYIBjom1bw4K7xuphQCzrSETYS6WqK0O
+thhU9e0STyTj152xT5paB5btzQtG2VnI54Hjf076D40CGxSjH4JbdVC4Ii4W9HEU
+fJfPmYTDJwI4P0APjAZMykAlmXQ5G3IGc1o+fetDCLETA7lnjJFFFcLiEUlqe30W
+DhN7kyiLJAPPNZ/m8292H4lWshoRVGnLu37+iN025QPxQSlOjdx51p9rdO/Sxl9v
+jOzm6gPMsw87iAqQAO3BE0VIIwdCx3IO6OgK+SAqBQIDAQABozowODAUBgNVHREE
+DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB
+MA0GCSqGSIb3DQEBCwUAA4IBAQAixYMBOk+cTh7Iu2R/92umf5m6hg7QRoEKjsrz
+yMCvCi+TtE7x08Uf4UtVz/iRaEVCGIxzBP7Ivq1Kb/p21FdK4oCp9i7O7mDwSicj
+258UMF2RiaI2L5ulNOg3Y0j5xGYnuLo0JJVKWfu0ZooY6nXIYrhUiJdYfTOhWlYd
+NDfeLWt+iFCwNhmzYZ9S1kimrdKnlTv8cXBHhvB3Wap2PIglFJNSSCOI+Y2hsss1
+PcshwJpFrupe46Cl91eTOCMmN8nHTjaWdDfmtwe3RGAwoTAwg1VXAEt8+GKX2G3Y
+4QpVea3dZiMZ5xdp3+5MQkd6tpuAh/Rt/bnbkjTZfX4dzh3q
+-----END CERTIFICATE-----

+ 28 - 0
localhost.key

@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJ5tPKKqEMoxF3
+gb4o5DIyirg1UDHrnv+0QXHWQuwNjuM9zTN92N0BvNUYFFxnBsiC2l85dJaeH3VN
+ggGOibVvDgrvG6mFALOtIRNhLpaorQ62GFT17RJPJOPXnbFPmloHlu3NC0bZWcjn
+geN/TvoPjQIbFKMfglt1ULgiLhb0cRR8l8+ZhMMnAjg/QA+MBkzKQCWZdDkbcgZz
+Wj5960MIsRMDuWeMkUUVwuIRSWp7fRYOE3uTKIskA881n+bzb3YfiVayGhFUacu7
+fv6I3TblA/FBKU6N3HnWn2t079LGX2+M7ObqA8yzDzuICpAA7cETRUgjB0LHcg7o
+6Ar5ICoFAgMBAAECggEAZSyXiuJry9JXaWSJryITCYK8JnMmYOVPS7vT2MO2WGD2
+/Lw8hl7K+RjwS93AJByhE/lr6HXFGD25YXq5Xv0g/yKxVfqtqymb4DEH7hYThzkZ
+eyScRpRjLWfWCPFLrhEx9kWqqqpw+W/lniVXpC8mXe6SVJG14I9JV3N4oiAofprb
+us88LZ6J/IAjDe4XafZCEbJUAXit44KjoNPX3EnL+kD0OwOg0jQm/Lec6ZybtU4B
+dDHmym91zl9I+vNFDZ22IYLvNaO5tRfDDyj8DOjuSnbKKUt1XKYhsb4nn7LOOx9t
+G2Hs8ODB9ys5I3Ha/s70xIvGIzV96o9TsRb2rLhpwQKBgQDvUt5asAQLuldDim52
+Rqvnv7We8spIGL1RsaqjAHf4Io6bGLFCdiNpSyCQKeDBn/t9fPf90TNhtuysevBO
+zMCjM+NdcVN0cCyQaONeoyw/7AeVU8kNlS7q15kKuL+uxmQZQ8SSsrIYab6wYYMw
+Ppmh4t5g/SkEDjJ8LT+T6kZgFQKBgQDX+Gmmc4qOmd7t9Ij4ATfopZNQPBlYwSkI
+IzMNJl2kts+YNq1JPCZmKfq+NzAedpu2J9TSFzANW28F5qgEdyNo76WQC6ENnlAs
+N2EQ33iMvsdk4hlyCnpreYjPnT7ttRa8M2qpvPAaQ02YN72R+F1mvpa6gVzOS0PP
+z/2O/9QuMQKBgGe1kYXbIOW0KnyqUJQQrA4RlbL7o4z6q4/rXfalDVgKIaI0YRxb
+9Zx7YfEXNL6zhVgr/4uOTKXbj1RvMhPzxsbyhWTI51FeCvHJgj5Ql6xbrC2Z0VaB
+f4NlSnzK8sXaUyZfp5+qsGLD8E6e8yrE9e6hUZzWGCAZxubo9NQ0garBAoGAfKiC
+MvWWEGF4b9kqBhyN7NdFhJZr+vLjgDLxELIIcz6h7LYCp7b0YxvwA0NPnwXPMwEi
+snF35v3Tw5AzgwBRjAxngBF2UKoElMESYe7bYkHsTarEDTNHHin0cgHNhN911APJ
+mVfZLw+SBj6GSW8WWmvADL8GlnyTE6x2Ksg37MECgYEA1sQwB7YyI/N5uK1zNSnw
+WndLgXs2n91yTyUVIfTlQmAUpsbUSROqabNlMLGmX37e4yODWAKt3nDgBZBkDwqX
+L6irnIzDrtBDJAk4Dc9LfsY9KZ0DRHri0kj5qE7GHcqVcYnn8+snzA55RzdHbDE6
+Ur+1MHOOJSTr0tc4N0vcxl8=
+-----END PRIVATE KEY-----

+ 92 - 0
main.flags.go

@@ -0,0 +1,92 @@
+package main
+
+import (
+	"flag"
+	"os"
+
+	apt "imuslab.com/arozos/mod/apt"
+	auth "imuslab.com/arozos/mod/auth"
+	db "imuslab.com/arozos/mod/database"
+	permission "imuslab.com/arozos/mod/permission"
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	System Flags
+*/
+
+//=========== SYSTEM PARAMTERS  ==============
+var sysdb *db.Database        //System database
+var authAgent *auth.AuthAgent //System authentication agent
+var permissionHandler *permission.PermissionHandler
+var userHandler *user.UserHandler         //User Handler
+var packageManager *apt.AptPackageManager //Manager for package auto installation
+var subserviceBasePort = 12810            //Next subservice port
+
+// =========== SYSTEM BUILD INFORMATION ==============
+var build_version = "development"                      //System build flag, this can be either {development / production / stable}
+var internal_version = "0.1.111"                       //Internal build version, please follow git commit counter for setting this value. max value \[0-9].[0-9][0-9].[0-9][0-9][0-9]\
+var deviceUUID string                                  //The device uuid of this host
+var deviceVendor = "IMUSLAB.INC"                       //Vendor of the system
+var deviceVendorURL = "http://imuslab.com"             //Vendor contact information
+var deviceModel = "AR100"                              //Hardware Model of the system
+var deviceModelDesc = "General Purpose Cloud Platform" //Device Model Description
+var iconVendor = "img/vendor/vendor_icon.png"          //Vendor icon location
+var iconSystem = "img/vendor/system_icon.png"          //System icon location
+
+// =========== RUNTTIME RELATED ================S
+var max_upload_size int64 = 8192 << 20                         //Maxmium upload size, default 8GB
+var sudo_mode bool = (os.Geteuid() == 0 || os.Geteuid() == -1) //Check if the program is launched as sudo mode or -1 on windows
+
+// =========== SYSTEM FLAGS ==============
+//Flags related to System startup
+var listen_port = flag.Int("port", 8080, "Listening port")
+var show_version = flag.Bool("version", false, "Show system build version")
+var host_name = flag.String("hostname", "My ArOZ", "Default name for this host")
+var system_uuid = flag.String("uuid", "", "System UUID for clustering and distributed computing. Only need to config once for first time startup. Leave empty for auto generation.")
+var disable_subservices = flag.Bool("disable_subservice", false, "Disable subservices completely")
+
+//Flags related to Networking
+var allow_upnp = flag.Bool("allow_upnp", false, "Enable uPNP service, recommended for host under NAT router")
+var allow_ssdp = flag.Bool("allow_ssdp", true, "Enable SSDP service, disable this if you do not want your device to be scanned by Windows's Network Neighborhood Page")
+var allow_mdns = flag.Bool("allow_mdns", true, "Enable MDNS service. Allow device to be scanned by nearby ArOZ Hosts")
+var disable_ip_resolve_services = flag.Bool("disable_ip_resolver", false, "Disable IP resolving if the system is running under reverse proxy environment")
+
+//Flags related to Security
+var use_tls = flag.Bool("tls", false, "Enable TLS on HTTP serving")
+var tls_cert = flag.String("cert", "localhost.crt", "TLS certificate file (.crt)")
+var session_key = flag.String("session_key", "", "Session key, must be 16, 24 or 32 bytes long (AES-128, AES-192 or AES-256). Leave empty for auto generated.")
+var tls_key = flag.String("key", "localhost.key", "TLS key file (.key)")
+
+//Flags related to hardware or interfaces
+var allow_hardware_management = flag.Bool("enable_hwman", true, "Enable hardware management functions in system")
+var wpa_supplicant_path = flag.String("wpa_supplicant_config", "/etc/wpa_supplicant/wpa_supplicant.conf", "Path for the wpa_supplicant config")
+var wan_interface_name = flag.String("wlan_interface_name", "wlan0", "The default wireless interface for connecting to an AP")
+
+//Flags related to files and uploads
+var max_upload = flag.Int("max_upload_size", 8192, "Maxmium upload size in MB. Must not exceed the available ram on your system")
+var upload_buf = flag.Int("upload_buf", 25, "Upload buffer memory in MB. Any file larger than this size will be buffered to disk (slower).")
+var storage_config_file = flag.String("storage_config", "./system/storage.json", "File location of the storage config file")
+var tmp_directory = flag.String("tmp", "./", "Temporary storage, can be access via tmp:/. A tmp/ folder will be created in this path. Recommend fast storage devices like SSD")
+var root_directory = flag.String("root", "./files/", "User root directories")
+var file_opr_buff = flag.Int("iobuf", 1024, "Amount of buffer memory for IO operations")
+var enable_dir_listing = flag.Bool("dir_list", true, "Enable directory listing")
+var enable_asyncFileUpload = flag.Bool("upload_async", false, "Enable file upload buffering to run in async mode (Faster upload, require RAM >= 8GB)")
+
+//Flags related to compatibility or testing
+var enable_beta_scanning_support = flag.Bool("beta_scan", false, "Allow compatibility to ArOZ Online Beta Clusters")
+var enable_console = flag.Bool("console", false, "Enable the debugging console.")
+
+//Flags related to running on Cloud Environment or public domain
+var allow_public_registry = flag.Bool("public_reg", false, "Enable public register interface for account creation")
+var allow_autologin = flag.Bool("allow_autologin", true, "Allow RESTFUL login redirection that allow machines like billboards to login to the system on boot")
+var demo_mode = flag.Bool("demo_mode", false, "Run the system in demo mode. All directories and database are read only.")
+var allow_package_autoInstall = flag.Bool("allow_pkg_install", true, "Allow the system to install package using Advanced Package Tool (aka apt or apt-get)")
+var allow_homepage = flag.Bool("enable_homepage", false, "Redirect not logged in users to home page instead of login interface")
+
+//Scheduling and System Service Related
+var nightlyTaskRunTime = flag.Int("ntt", 3, "Nightly tasks execution time. Default 3 = 3 am in the morning")
+var maxTempFileKeepTime = flag.Int("tmp_time", 86400, "Time before tmp file will be deleted in seconds. Default 86400 seconds = 24 hours")
+
+//Flags related to ArozOS Cluster services
+var allow_clustering = flag.Bool("allow_cluster", true, "Enable cluster operations within LAN. Require allow_mdns=true flag")

+ 141 - 0
main.go

@@ -0,0 +1,141 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"strconv"
+	"syscall"
+
+	console "imuslab.com/arozos/mod/console"
+)
+
+/*
+	arozos
+	author: tobychui
+
+	To edit startup flags, see main.flag.go
+	To edit main routing logic, see main.router.go
+	To edit startup sequence, see startup.go
+
+	P.S. Try to keep this file < 300 lines
+*/
+
+//Close handler, close db and clearn up everything before exit
+func SetupCloseHandler() {
+	c := make(chan os.Signal, 2)
+	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
+	go func() {
+		<-c
+		executeShutdownSequence()
+	}()
+}
+
+func executeShutdownSequence() {
+	//Shutdown authAgent
+	log.Println("\r- Shutting down auth gateway")
+	authAgent.Close()
+
+	//Shutdown file system handler db
+	log.Println("\r- Shutting down fsdb")
+	CloseAllStorages()
+
+	//Shutdown Subservices
+	log.Println("\r- Shutting down background subservices")
+	//system_subservice_handleShutdown()
+
+	//Shutdown database
+	log.Println("\r- Shutting down database")
+	sysdb.Close()
+
+	//Shutdown network services
+	StopNetworkServices()
+
+	//Shutdown FTP Server
+	if ftpServer != nil {
+		log.Println("\r- Shutting down FTP Server")
+		ftpServer.Close()
+	}
+
+	//Cleaning up tmp files
+	log.Println("\r- Cleaning up tmp folder")
+	os.RemoveAll(*tmp_directory)
+	//Do other things
+	os.Exit(0)
+}
+
+func main() {
+	//Parse startup flags and paramters
+	flag.Parse()
+
+	//Handle version printing
+	if *show_version {
+		fmt.Println("ArozOS " + build_version + " Revision " + internal_version)
+		fmt.Println("Developed by tobychui and other co-developers, Licensed to " + deviceVendor)
+		//fmt.Println("THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.")
+		os.Exit(0)
+	}
+
+	//Handle flag assignments
+	max_upload_size = int64(*max_upload) << 20 //Parse the max upload size
+	if *demo_mode {                            //Disable hardware man under demo mode
+		enablehw := false
+		allow_hardware_management = &enablehw
+	}
+
+	//Setup handler for Ctrl +C
+	SetupCloseHandler()
+
+	//Clean up previous tmp files
+	final_tmp_directory := filepath.Clean(*tmp_directory) + "/tmp/"
+	tmp_directory = &final_tmp_directory
+	os.RemoveAll(*tmp_directory)
+	os.Mkdir(*tmp_directory, 0777)
+
+	//Print copyRight information
+	log.Println("ArozOS(C) 2020 " + deviceVendor + ".")
+	log.Println("ArozOS " + build_version + " Revision " + internal_version)
+
+	//Setup homepage folder if not exists
+	if !fileExists("./web/homepage") {
+		os.MkdirAll("./web/homepage", 0644)
+	}
+	/*
+		New Implementation of the ArOZ Online System, Sept 2020
+	*/
+	RunStartup()
+
+	//Initiate all the static files transfer
+	fs := http.FileServer(http.Dir("./web"))
+	http.Handle("/", mroutner(fs))
+
+	//Set database read write to ReadOnly after startup if demo mode
+	if *demo_mode {
+		sysdb.UpdateReadWriteMode(true)
+	}
+
+	//Start http server
+	go func() {
+		if *use_tls {
+			log.Println("Secure Web server listening at :" + strconv.Itoa(*listen_port))
+			http.ListenAndServeTLS(":"+strconv.Itoa(*listen_port), *tls_cert, *tls_key, nil)
+		} else {
+			log.Println("Web server listening at :" + strconv.Itoa(*listen_port))
+			http.ListenAndServe(":"+strconv.Itoa(*listen_port), nil)
+		}
+	}()
+
+	if *enable_console == true {
+		//Startup interactive shell for debug and basic controls
+		Console := console.NewConsole(consoleCommandHandler)
+		Console.ListenAndHandle()
+	} else {
+		//Just do a blocking loop here
+		select {}
+	}
+
+}

+ 156 - 0
main.router.go

@@ -0,0 +1,156 @@
+package main
+
+/*
+	ArOZ Online System Main Request Router
+
+	This is used to check authentication before actually serving file to the target client
+	This function also handle the special page (login.system and user.system) delivery
+*/
+
+import (
+	"net/http"
+	"path/filepath"
+	"strconv"
+	"strings"
+
+	fs "imuslab.com/arozos/mod/filesystem"
+)
+
+func mroutner(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		/*
+			You can also check the path for url using r.URL.Path
+		*/
+
+		if r.URL.Path == "/favicon.ico" || r.URL.Path == "/manifest.webmanifest" {
+			//Serving favicon or manifest. Allow no auth access.
+			h.ServeHTTP(w, r)
+		} else if r.URL.Path == "/login.system" {
+			//Login page. Require special treatment for template.
+			//Get the redirection address from the request URL
+			red, _ := mv(r, "redirect", false)
+
+			//Append the redirection addr into the template
+			imgsrc := "./web/" + iconSystem
+			if !fileExists(imgsrc) {
+				imgsrc = "./web/img/public/auth_icon.png"
+			}
+			imageBase64, _ := LoadImageAsBase64(imgsrc)
+			parsedPage, err := template_load("web/login.system", map[string]interface{}{
+				"redirection_addr": red,
+				"usercount":        strconv.Itoa(authAgent.GetUserCounts()),
+				"service_logo":     imageBase64,
+			})
+			if err != nil {
+				panic("Error. Unable to parse login page. Is web directory data exists?")
+			}
+			w.Write([]byte(parsedPage))
+		} else if r.URL.Path == "/reset.system" && authAgent.GetUserCounts() > 0 {
+			//Password restart page. Allow access only when user number > 0
+			system_resetpw_handlePasswordReset(w, r)
+		} else if r.URL.Path == "/user.system" && authAgent.GetUserCounts() == 0 {
+			//Serve user management page. This only allows serving of such page when the total usercount = 0 (aka System Initiation)
+			h.ServeHTTP(w, r)
+
+		} else if (len(r.URL.Path) > 11 && r.URL.Path[:11] == "/img/public") || (len(r.URL.Path) > 7 && r.URL.Path[:7] == "/script") {
+			//Public image directory. Allow anyone to access resources inside this directory.
+			if filepath.Ext("web"+fs.DecodeURI(r.RequestURI)) == ".js" {
+				//Fixed serve js meme type invalid bug on Firefox
+				w.Header().Add("Content-Type", "application/javascript; charset=UTF-8")
+			}
+			h.ServeHTTP(w, r)
+		} else if len(r.URL.Path) >= len("/webdav") && r.URL.Path[:7] == "/webdav" {
+			WebDavHandler.HandleRequest(w, r)
+		} else if r.URL.Path == "/" && authAgent.CheckAuth(r) {
+			//Use logged in and request the index. Serve the user's interface module
+			w.Header().Set("Cache-Control", "no-cache, no-store, no-transform, must-revalidate, private, max-age=0")
+			userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+			if err != nil {
+				//ERROR!! Server default
+				h.ServeHTTP(w, r)
+			} else {
+				interfaceModule := userinfo.GetInterfaceModules()
+				if len(interfaceModule) == 1 && interfaceModule[0] == "Desktop" {
+					http.Redirect(w, r, "desktop.system", 307)
+				} else if len(interfaceModule) == 1 {
+					//User with default interface module not desktop
+					modileInfo := moduleHandler.GetModuleInfoByID(interfaceModule[0])
+					http.Redirect(w, r, modileInfo.StartDir, 307)
+				} else if len(interfaceModule) > 1 {
+					//Redirect to module selector
+					http.Redirect(w, r, "SystemAO/boot/interface_selector.html", 307)
+				} else if len(interfaceModule) == 0 {
+					//Redirect to error page
+					http.Redirect(w, r, "SystemAO/boot/no_interfaceing.html", 307)
+				} else {
+					//For unknown operations, send it to desktop
+					http.Redirect(w, r, "desktop.system", 307)
+				}
+			}
+		} else if r.URL.Path == "/" && !authAgent.CheckAuth(r) && *allow_homepage == true {
+			//User not logged in but request the index, redirect to homepage
+			http.Redirect(w, r, "/homepage/index.html", 307)
+		} else if authAgent.CheckAuth(r) {
+			//User logged in. Continue to serve the file the client want
+			authAgent.UpdateSessionExpireTime(w, r)
+			if build_version == "development" {
+				//Disable caching when under development build
+				//w.Header().Set("Cache-Control", "no-cache, no-store, no-transform, must-revalidate, private, max-age=0")
+			}
+			if filepath.Ext("web"+fs.DecodeURI(r.RequestURI)) == ".js" {
+				//Fixed serve js meme type invalid bug on Firefox
+				w.Header().Add("Content-Type", "application/javascript; charset=UTF-8")
+			}
+
+			if *disable_subservices == false {
+				//Enable subservice access
+				//Check if this path is reverse proxy path. If yes, serve with proxyserver
+				isRP, proxy, rewriteURL, subserviceObject := ssRouter.CheckIfReverseProxyPath(r)
+
+				if isRP {
+					//Check user permission on that module
+					ssRouter.HandleRoutingRequest(w, r, proxy, subserviceObject, rewriteURL)
+					return
+				}
+			}
+
+			//Not subservice routine. Handle file server
+			if *enable_dir_listing == false {
+				if strings.HasSuffix(r.URL.Path, "/") {
+					//User trying to access a directory. Send NOT FOUND.
+					if fileExists("web" + r.URL.Path + "index.html") {
+						//Index exists. Allow passthrough
+
+					} else {
+						errorHandleNotFound(w, r)
+						return
+					}
+				}
+			}
+			h.ServeHTTP(w, r)
+
+		} else {
+			//User not logged in. Check if the path end with public/. If yes, allow public access
+			if r.URL.Path[len(r.URL.Path)-1:] != "/" && filepath.Base(filepath.Dir(r.URL.Path)) == "public" {
+				//This file path end with public/. Allow public access
+				h.ServeHTTP(w, r)
+			} else if *allow_homepage == true && r.URL.Path[:10] == "/homepage/" {
+				//Handle public home serving if homepage mode is enabled
+				h.ServeHTTP(w, r)
+			} else {
+				//Other paths
+				if *allow_homepage {
+					//Redirect to home page
+					http.Redirect(w, r, "/homepage/index.html", 307)
+				} else {
+					//Rediect to login page
+					w.Header().Set("Cache-Control", "no-cache, no-store, no-transform, must-revalidate, private, max-age=0")
+					http.Redirect(w, r, "/login.system?redirect="+r.URL.Path, 307)
+				}
+
+			}
+
+		}
+
+	})
+}

+ 151 - 0
mediaServer.go

@@ -0,0 +1,151 @@
+package main
+
+import (
+	"errors"
+	"log"
+	"net/http"
+	"net/url"
+	"path/filepath"
+	"strings"
+
+	fs "imuslab.com/arozos/mod/filesystem"
+)
+
+/*
+Media Server
+This function serve large file objects like video and audio file via asynchronize go routine :)
+
+Example usage:
+/media/?file=user:/Desktop/test/02.Orchestra- エミール (Addendum version).mp3
+/media/?file=user:/Desktop/test/02.Orchestra- エミール (Addendum version).mp3&download=true
+
+This will serve / download the file located at files/users/{username}/Desktop/test/02.Orchestra- エミール (Addendum version).mp3
+
+PLEASE ALWAYS USE URLENCODE IN THE LINK PASSED INTO THE /media ENDPOINT
+
+
+*/
+
+//
+
+func mediaServer_init() {
+	http.HandleFunc("/media/", serverMedia)
+	http.HandleFunc("/media/getMime/", serveMediaMime)
+}
+
+//This function validate the incoming media request and return the real path for the targed file
+func media_server_validateSourceFile(w http.ResponseWriter, r *http.Request) (string, error) {
+	username, err := authAgent.GetUserName(w, r)
+	if err != nil {
+		return "", errors.New("User not logged in")
+	}
+
+	userinfo, _ := userHandler.GetUserInfoFromUsername(username)
+
+	//Validate url valid
+	if strings.Count(r.URL.String(), "?") > 1 {
+		return "", errors.New("Invalid paramters. Multiple ? found")
+	}
+
+	targetfile, _ := mv(r, "file", false)
+	targetfile, _ = url.QueryUnescape(targetfile)
+	if targetfile == "" {
+		return "", errors.New("Missing paramter 'file'")
+	}
+
+	//Translate the virtual directory to realpath
+	realFilepath, err := userinfo.VirtualPathToRealPath(targetfile)
+	if fileExists(realFilepath) && IsDir(realFilepath) {
+		return "", errors.New("Given path is not a file.")
+	}
+	if err != nil {
+		return "", errors.New("Unable to translate the given filepath")
+	}
+
+	if !fileExists(realFilepath) {
+		//Sometime if url is not URL encoded, this error might be shown as well
+
+		//Try to use manual segmentation
+
+		originalURL := r.URL.String()
+		//Must be pre-processed with system special URI Decode function to handle edge cases
+		originalURL = fs.DecodeURI(originalURL)
+		if strings.Contains(originalURL, "&download=true") {
+			originalURL = strings.ReplaceAll(originalURL, "&download=true", "")
+		} else if strings.Contains(originalURL, "download=true") {
+			originalURL = strings.ReplaceAll(originalURL, "download=true", "")
+		}
+		if strings.Contains(originalURL, "&file=") {
+			originalURL = strings.ReplaceAll(originalURL, "&file=", "file=")
+		}
+		urlInfo := strings.Split(originalURL, "file=")
+		possibleVirtualFilePath := urlInfo[len(urlInfo)-1]
+		possibleRealpath, err := userinfo.VirtualPathToRealPath(possibleVirtualFilePath)
+		if err != nil {
+			log.Println("Error when trying to serve file in compatibility mode", err.Error())
+			return "", errors.New("Error when trying to serve file in compatibility mode")
+		}
+		if fileExists(possibleRealpath) {
+			realFilepath = possibleRealpath
+			log.Println("[Media Server] Serving file " + filepath.Base(possibleRealpath) + " in compatibility mode. Do not to use '&' or '+' sign in filename! ")
+			return realFilepath, nil
+		} else {
+			return "", errors.New("File not exists")
+		}
+	}
+
+	return realFilepath, nil
+}
+
+func serveMediaMime(w http.ResponseWriter, r *http.Request) {
+	realFilepath, err := media_server_validateSourceFile(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+	mime := "text/directory"
+	if !IsDir(realFilepath) {
+		m, _, err := fs.GetMime(realFilepath)
+		if err != nil {
+			mime = ""
+		}
+		mime = m
+	}
+
+	sendTextResponse(w, mime)
+}
+
+func serverMedia(w http.ResponseWriter, r *http.Request) {
+	//Serve normal media files
+	realFilepath, err := media_server_validateSourceFile(w, r)
+	if err != nil {
+		sendErrorResponse(w, err.Error())
+		return
+	}
+
+	//Check if downloadMode
+	downloadMode := false
+	dw, _ := mv(r, "download", false)
+	if dw == "true" {
+		downloadMode = true
+	}
+
+	//Serve the file
+	if downloadMode {
+		userAgent := r.Header.Get("User-Agent")
+		filename := strings.ReplaceAll(url.QueryEscape(filepath.Base(realFilepath)), "+", "%20")
+		log.Println(r.Header.Get("User-Agent"))
+
+		if strings.Contains(userAgent, "Safari/") {
+			//This is Safari. Use speial header
+			w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(realFilepath))
+			w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
+		} else {
+			//Fixing the header issue on Golang url encode lib problems
+			w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+filename)
+			w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
+		}
+	}
+
+	http.ServeFile(w, r, realFilepath)
+}

+ 29 - 0
mod/agi/agi.audio.go

@@ -0,0 +1,29 @@
+package agi
+
+import (
+	"log"
+
+	"github.com/robertkrimen/otto"
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	AJGI Audio Library
+
+	This is a library for allowing audio playback from AGI script
+	Powered by Go Beep and the usage might be a bit tricky
+
+	Author: tobychui
+
+*/
+
+func (g *Gateway) AudioLibRegister() {
+	err := g.RegisterLib("audio", g.injectAudioFunctions)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func (g *Gateway) injectAudioFunctions(vm *otto.Otto, u *user.User) {
+
+}

+ 585 - 0
mod/agi/agi.file.go

@@ -0,0 +1,585 @@
+package agi
+
+import (
+	"errors"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+
+	"github.com/robertkrimen/otto"
+	fs "imuslab.com/arozos/mod/filesystem"
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	AJGI File Processing Library
+
+	This is a library for handling image related functionalities in agi scripts.
+
+	By Alanyueng 2020 <- This person write shitty code that need me to tidy up (by tobychui)
+	Complete rewrite by tobychui in Sept 2020
+*/
+
+func (g *Gateway) FileLibRegister() {
+	err := g.RegisterLib("filelib", g.injectFileLibFunctions)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func (g *Gateway) injectFileLibFunctions(vm *otto.Otto, u *user.User) {
+
+	//Legacy File system API
+	//writeFile(virtualFilepath, content) => return true/false when succeed / failed
+	vm.Set("_filelib_writeFile", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check for permission
+		if !u.CanWrite(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied: "+vpath))
+		}
+
+		content, err := call.Argument(1).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check if there is quota for the given length
+		if !u.StorageQuota.HaveSpace(int64(len(content))) {
+			//User have no remaining storage quota
+			g.raiseError(errors.New("Storage Quota Fulled"))
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Translate the virtual path to realpath
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check if file already exists.
+		if fileExists(rpath) {
+			//Check if this user own this file
+			isOwner := u.IsOwnerOfFile(rpath)
+			if isOwner {
+				//This user own this system. Remove this file from his quota
+				u.RemoveOwnershipFromFile(rpath)
+			}
+		}
+
+		//Create and write to file using ioutil
+		err = ioutil.WriteFile(rpath, []byte(content), 0755)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Add the filesize to user quota
+		u.SetOwnerOfFile(rpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	vm.Set("_filelib_deleteFile", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check for permission
+		if !u.CanWrite(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied: "+vpath))
+		}
+
+		//Translate the virtual path to realpath
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check if file already exists.
+		if fileExists(rpath) {
+			//Check if this user own this file
+			isOwner := u.IsOwnerOfFile(rpath)
+			if isOwner {
+				//This user own this system. Remove this file from his quota
+				u.RemoveOwnershipFromFile(rpath)
+			}
+		} else {
+			g.raiseError(errors.New("File not exists"))
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Remove the file
+		os.Remove(rpath)
+
+		reply, _ := vm.ToValue(true)
+		return reply
+	})
+
+	//readFile(virtualFilepath) => return content in string
+	vm.Set("_filelib_readFile", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check for permission
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied: "+vpath))
+		}
+
+		//Translate the virtual path to realpath
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Create and write to file using ioUtil
+		content, err := ioutil.ReadFile(rpath)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		reply, _ := vm.ToValue(string(content))
+		return reply
+	})
+
+	//Listdir
+	//readdir("user:/Desktop") => return filelist in array
+	vm.Set("_filelib_readdir", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Translate the virtual path to realpath
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		fileList, err := specialGlob(rpath)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Translate all paths to virtual paths
+		results := []string{}
+		for _, file := range fileList {
+			if IsDir(file) {
+				thisRpath, _ := realpathToVirtualpath(file, u)
+				results = append(results, thisRpath)
+			}
+		}
+
+		reply, _ := vm.ToValue(results)
+		return reply
+	})
+
+	//Usage
+	//filelib.walk("user:/") => list everything recursively
+	//filelib.walk("user:/", "folder") => list all folder recursively
+	//filelib.walk("user:/", "file") => list all files recursively
+	vm.Set("_filelib_walk", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		mode, err := call.Argument(1).ToString()
+		if err != nil {
+			mode = "all"
+		}
+
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		results := []string{}
+		err = filepath.Walk(rpath, func(path string, info os.FileInfo, err error) error {
+			thisVpath, err := realpathToVirtualpath(path, u)
+			if mode == "file" {
+				if !info.IsDir() {
+					results = append(results, thisVpath)
+				}
+			} else if mode == "folder" {
+				if info.IsDir() {
+					results = append(results, thisVpath)
+				}
+			} else {
+				results = append(results, thisVpath)
+			}
+
+			return nil
+		})
+
+		reply, _ := vm.ToValue(results)
+		return reply
+	})
+
+	//Glob
+	//glob("user:/Desktop/*.mp3") => return fileList in array
+	//glob("/") => return a list of root directories
+	vm.Set("_filelib_glob", func(call otto.FunctionCall) otto.Value {
+		regex, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Handle when regex = "." or "./" (listroot)
+		if filepath.ToSlash(filepath.Clean(regex)) == "/" || filepath.Clean(regex) == "." {
+			//List Root
+			rootDirs := []string{}
+			fileHandlers := u.GetAllFileSystemHandler()
+			for _, fsh := range fileHandlers {
+				rootDirs = append(rootDirs, fsh.UUID+":/")
+			}
+
+			reply, _ := vm.ToValue(rootDirs)
+			return reply
+		} else {
+			//Check for permission
+			if !u.CanRead(regex) {
+				panic(vm.MakeCustomError("PermissionDenied", "Path access denied"))
+			}
+			//This function can only handle wildcard in filename but not in dir name
+			vrootPath := filepath.Dir(regex)
+			regexFilename := filepath.Base(regex)
+			//Translate the virtual path to realpath
+			rrootPath, err := virtualPathToRealPath(vrootPath, u)
+			if err != nil {
+				g.raiseError(err)
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			suitableFiles, err := filepath.Glob(rrootPath + "/" + regexFilename)
+			if err != nil {
+				g.raiseError(err)
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			results := []string{}
+			for _, file := range suitableFiles {
+				thisRpath, _ := realpathToVirtualpath(filepath.ToSlash(file), u)
+				results = append(results, thisRpath)
+			}
+			reply, _ := vm.ToValue(results)
+			return reply
+		}
+	})
+
+	//Advance Glob using file system special Glob, cannot use to scan root dirs
+	vm.Set("_filelib_aglob", func(call otto.FunctionCall) otto.Value {
+		regex, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		if regex != "/" && !u.CanRead(regex) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied"))
+		}
+
+		//This function can only handle wildcard in filename but not in dir name
+		vrootPath := filepath.Dir(regex)
+		regexFilename := filepath.Base(regex)
+		//Translate the virtual path to realpath
+		rrootPath, err := virtualPathToRealPath(vrootPath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		suitableFiles, err := specialGlob(rrootPath + "/" + regexFilename)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		results := []string{}
+		for _, file := range suitableFiles {
+			thisRpath, _ := realpathToVirtualpath(filepath.ToSlash(file), u)
+			results = append(results, thisRpath)
+		}
+		reply, _ := vm.ToValue(results)
+		return reply
+	})
+
+	//filesize("user:/Desktop/test.txt")
+	vm.Set("_filelib_filesize", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check for permission
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied"))
+		}
+
+		//Translate the virtual path to realpath
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Get filesize of file
+		rawsize := fs.GetFileSize(rpath)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		reply, _ := vm.ToValue(rawsize)
+		return reply
+	})
+
+	//fileExists("user:/Desktop/test.txt") => return true / false
+	vm.Set("_filelib_fileExists", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check for permission
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied"))
+		}
+
+		//Translate the virtual path to realpath
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		if fileExists(rpath) {
+			reply, _ := vm.ToValue(true)
+			return reply
+		} else {
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+	})
+
+	//fileExists("user:/Desktop/test.txt") => return true / false
+	vm.Set("_filelib_isDir", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check for permission
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied: "+vpath))
+		}
+
+		//Translate the virtual path to realpath
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		if _, err := os.Stat(rpath); os.IsNotExist(err) {
+			//File not exists
+			panic(vm.MakeCustomError("File Not Exists", "Required path not exists"))
+		}
+
+		if IsDir(rpath) {
+			reply, _ := vm.ToValue(true)
+			return reply
+		} else {
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+	})
+
+	//Make directory command
+	vm.Set("_filelib_mkdir", func(call otto.FunctionCall) otto.Value {
+		vdir, err := call.Argument(0).ToString()
+		if err != nil {
+			return otto.FalseValue()
+		}
+
+		//Check for permission
+		if !u.CanWrite(vdir) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied"))
+		}
+
+		//Translate the path to realpath
+		rdir, err := virtualPathToRealPath(vdir, u)
+		if err != nil {
+			log.Println(err.Error())
+			return otto.FalseValue()
+		}
+
+		//Create the directory at rdir location
+		err = os.MkdirAll(rdir, 0755)
+		if err != nil {
+			log.Println(err.Error())
+			return otto.FalseValue()
+		}
+
+		return otto.TrueValue()
+	})
+
+	//Get MD5 of the given filepath
+	vm.Set("_filelib_md5", func(call otto.FunctionCall) otto.Value {
+		log.Println("Call to MD5 Functions!")
+		return otto.FalseValue()
+	})
+
+	//Get the root name of the given virtual path root
+	vm.Set("_filelib_rname", func(call otto.FunctionCall) otto.Value {
+		//Get virtual path from the function input
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Get fs handler from the vpath
+		fsHandler, err := u.GetFileSystemHandlerFromVirtualPath(vpath)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Return the name of the fsHandler
+		name, _ := vm.ToValue(fsHandler.Name)
+		return name
+
+	})
+
+	vm.Set("_filelib_mtime", func(call otto.FunctionCall) otto.Value {
+		vpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check for permission
+		if !u.CanRead(vpath) {
+			panic(vm.MakeCustomError("PermissionDenied", "Path access denied"))
+		}
+
+		parseToUnix, err := call.Argument(1).ToBoolean()
+		if err != nil {
+			parseToUnix = false
+		}
+
+		rpath, err := virtualPathToRealPath(vpath, u)
+		if err != nil {
+			log.Println(err.Error())
+			return otto.FalseValue()
+		}
+
+		info, err := os.Stat(rpath)
+		if err != nil {
+			log.Println(err.Error())
+			return otto.FalseValue()
+		}
+
+		modTime := info.ModTime()
+		if parseToUnix {
+			result, _ := otto.ToValue(modTime.Unix())
+			return result
+		} else {
+			result, _ := otto.ToValue(modTime.Format("2006-01-02 15:04:05"))
+			return result
+		}
+	})
+
+	/*
+		vm.Set("_filelib_decodeURI", func(call otto.FunctionCall) otto.Value {
+			originalURI, err := call.Argument(0).ToString()
+			if err != nil {
+				g.raiseError(err)
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+			decodedURI := specialURIDecode(originalURI)
+			result, err := otto.ToValue(decodedURI)
+			if err != nil {
+				g.raiseError(err)
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+			return result
+		})
+	*/
+
+	//Other file operations, wip
+
+	//Wrap all the native code function into an imagelib class
+	vm.Run(`
+		var filelib = {};
+		filelib.writeFile = _filelib_writeFile;
+		filelib.readFile = _filelib_readFile;
+		filelib.deleteFile = _filelib_deleteFile;
+		filelib.readdir = _filelib_readdir;
+		filelib.walk = _filelib_walk;
+		filelib.glob = _filelib_glob;
+		filelib.aglob = _filelib_aglob;
+		filelib.filesize = _filelib_filesize;
+		filelib.fileExists = _filelib_fileExists;
+		filelib.isDir = _filelib_isDir;
+		filelib.md5 = _filelib_md5;
+		filelib.mkdir = _filelib_mkdir;
+		filelib.mtime = _filelib_mtime;
+		filelib.rname = _filelib_rname;
+	`)
+}

+ 313 - 0
mod/agi/agi.go

@@ -0,0 +1,313 @@
+package agi
+
+import (
+	"errors"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"path/filepath"
+	"strings"
+
+	"github.com/robertkrimen/otto"
+
+	apt "imuslab.com/arozos/mod/apt"
+	auth "imuslab.com/arozos/mod/auth"
+	metadata "imuslab.com/arozos/mod/filesystem/metadata"
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	ArOZ Online Javascript Gateway Interface (AGI)
+	author: tobychui
+
+	This script load plugins written in Javascript and run them in VM inside golang
+	DO NOT CONFUSE PLUGIN WITH SUBSERVICE :))
+*/
+
+var (
+	AgiVersion string = "1.4" //Defination of the agi runtime version. Update this when new function is added
+)
+
+type AgiLibIntergface func(*otto.Otto, *user.User) //Define the lib loader interface for AGI Libraries
+type AgiPackage struct {
+	InitRoot string //The initialization of the root for the module that request this package
+}
+
+type AgiSysInfo struct {
+	//System information
+	BuildVersion    string
+	InternalVersion string
+	LoadedModule    []string
+
+	//System Handlers
+	UserHandler          *user.UserHandler
+	ReservedTables       []string
+	PackageManager       *apt.AptPackageManager
+	ModuleRegisterParser func(string) error
+	AuthAgent            *auth.AuthAgent
+	FileSystemRender     *metadata.RenderHandler
+
+	//Scanning Roots
+	StartupRoot   string
+	ActivateScope []string
+}
+
+type Gateway struct {
+	ReservedTables   []string
+	AllowAccessPkgs  map[string][]AgiPackage
+	LoadedAGILibrary map[string]AgiLibIntergface
+	Option           *AgiSysInfo
+}
+
+func NewGateway(option AgiSysInfo) (*Gateway, error) {
+	//Handle startup registration of ajgi modules
+	startupScripts, _ := filepath.Glob(filepath.ToSlash(filepath.Clean(option.StartupRoot)) + "/*/init.agi")
+	gatewayObject := Gateway{
+		ReservedTables:   option.ReservedTables,
+		AllowAccessPkgs:  map[string][]AgiPackage{},
+		LoadedAGILibrary: map[string]AgiLibIntergface{},
+		Option:           &option,
+	}
+
+	for _, script := range startupScripts {
+		scriptContentByte, _ := ioutil.ReadFile(script)
+		scriptContent := string(scriptContentByte)
+		log.Println("Gatewat script loaded (" + script + ")")
+		//Create a new vm for this request
+		vm := otto.New()
+
+		//Only allow non user based operations
+		gatewayObject.injectStandardLibs(vm, script, "./web/")
+
+		_, err := vm.Run(scriptContent)
+		if err != nil {
+			log.Println("AJI Load Failed: " + script + ". Skipping.")
+			log.Println(err)
+			continue
+		}
+	}
+
+	//Load all the other libs entry points into the memoary
+	gatewayObject.ImageLibRegister()
+	gatewayObject.FileLibRegister()
+	gatewayObject.HTTPLibRegister()
+
+	return &gatewayObject, nil
+}
+
+func (g *Gateway) RunScript(script string) error {
+	//Create a new vm for this request
+	vm := otto.New()
+
+	//Only allow non user based operations
+	g.injectStandardLibs(vm, "", "./web/")
+
+	_, err := vm.Run(script)
+	if err != nil {
+		log.Println("Script Execution Failed: ", err.Error())
+		return err
+	}
+
+	return nil
+}
+
+func (g *Gateway) RegisterLib(libname string, entryPoint AgiLibIntergface) error {
+	_, ok := g.LoadedAGILibrary[libname]
+	if ok {
+		//This lib already registered. Return error
+		return errors.New("This library name already registered")
+	} else {
+		g.LoadedAGILibrary[libname] = entryPoint
+	}
+	return nil
+}
+
+func (g *Gateway) raiseError(err error) {
+	log.Println("*AGI Engine* [Runtime Error] " + err.Error())
+
+	//To be implemented
+}
+
+//Check if this table is restricted table. Return true if the access is valid
+func (g *Gateway) filterDBTable(tablename string, existsCheck bool) bool {
+	//Check if table is restricted
+	if stringInSlice(tablename, g.ReservedTables) {
+		return false
+	}
+
+	//Check if table exists
+	if existsCheck {
+		if !g.Option.UserHandler.GetDatabase().TableExists(tablename) {
+			return false
+		}
+	}
+
+	return true
+}
+
+//Handle request from RESTFUL API
+func (g *Gateway) APIHandler(w http.ResponseWriter, r *http.Request, thisuser *user.User) {
+	scriptContent, err := mv(r, "script", true)
+	if err != nil {
+		w.WriteHeader(http.StatusBadRequest)
+		w.Write([]byte("400 - Bad Request (Missing script content)"))
+		return
+	}
+	g.ExecuteAGIScript(scriptContent, "", "", w, r, thisuser)
+}
+
+//Handle user requests
+func (g *Gateway) InterfaceHandler(w http.ResponseWriter, r *http.Request, thisuser *user.User) {
+	//Get user object from the request
+	startupRoot := g.Option.StartupRoot
+	startupRoot = filepath.ToSlash(filepath.Clean(startupRoot))
+
+	//Get the script files for the plugin
+	scriptFile, err := mv(r, "script", false)
+	if err != nil {
+		sendErrorResponse(w, "Invalid script path")
+		return
+	}
+	scriptFile = specialURIDecode(scriptFile)
+
+	//Check if the script path exists
+	scriptExists := false
+	scriptScope := "./web/"
+	for _, thisScope := range g.Option.ActivateScope {
+		thisScope = filepath.ToSlash(filepath.Clean(thisScope))
+		if fileExists(thisScope + "/" + scriptFile) {
+			scriptExists = true
+			scriptFile = thisScope + "/" + scriptFile
+			scriptScope = thisScope
+		}
+	}
+
+	if !scriptExists {
+		sendErrorResponse(w, "Script not found")
+		return
+	}
+
+	//Check for user permission on this module
+	moduleName := getScriptRoot(scriptFile, scriptScope)
+	if !thisuser.GetModuleAccessPermission(moduleName) {
+		w.WriteHeader(http.StatusForbidden)
+		if g.Option.BuildVersion == "development" {
+			w.Write([]byte("Permission denied: User do not have permission to access " + moduleName))
+		} else {
+			w.Write([]byte("403 Forbidden"))
+		}
+
+		return
+	}
+
+	//Check the given file is actually agi script
+	if !(filepath.Ext(scriptFile) == ".agi" || filepath.Ext(scriptFile) == ".js") {
+		w.WriteHeader(http.StatusForbidden)
+
+		if g.Option.BuildVersion == "development" {
+			w.Write([]byte("AGI script must have file extension of .agi or .js"))
+		} else {
+			w.Write([]byte("403 Forbidden"))
+		}
+
+		return
+	}
+
+	//Get the content of the script
+	scriptContentByte, _ := ioutil.ReadFile(scriptFile)
+	scriptContent := string(scriptContentByte)
+
+	g.ExecuteAGIScript(scriptContent, scriptFile, scriptScope, w, r, thisuser)
+}
+
+/*
+	Executing the given AGI Script contents. Requires:
+	scriptContent: The AGI command sequence
+	scriptFile: The filepath of the script file
+	scriptScope: The scope of the script file, aka the module base path
+	w / r : Web request and response writer
+	thisuser: userObject
+
+*/
+func (g *Gateway) ExecuteAGIScript(scriptContent string, scriptFile string, scriptScope string, w http.ResponseWriter, r *http.Request, thisuser *user.User) {
+	//Create a new vm for this request
+	vm := otto.New()
+	//Inject standard libs into the vm
+	g.injectStandardLibs(vm, scriptFile, scriptScope)
+	g.injectUserFunctions(vm, thisuser, w, r)
+
+	//Detect cotent type
+	contentType := r.Header.Get("Content-type")
+	if strings.Contains(contentType, "application/json") {
+		//For shitty people who use Angular
+		body, _ := ioutil.ReadAll(r.Body)
+		vm.Set("POST_data", string(body))
+	} else {
+		r.ParseForm()
+		//Insert all paramters into the vm
+		for k, v := range r.PostForm {
+			if len(v) == 1 {
+				vm.Set(k, v[0])
+			} else {
+				vm.Set(k, v)
+			}
+
+		}
+	}
+
+	_, err := vm.Run(scriptContent)
+	if err != nil {
+		scriptpath, _ := filepath.Abs(scriptFile)
+		g.RenderErrorTemplate(w, err.Error(), scriptpath)
+		return
+	}
+
+	//Get the return valu from the script
+	value, err := vm.Get("HTTP_RESP")
+	if err != nil {
+		sendTextResponse(w, "")
+		return
+	}
+	valueString, err := value.ToString()
+
+	//Get respond header type from the vm
+	header, _ := vm.Get("HTTP_HEADER")
+	headerString, _ := header.ToString()
+	if headerString != "" {
+		w.Header().Set("Content-Type", headerString)
+	}
+
+	w.Write([]byte(valueString))
+}
+
+/*
+	Execute AGI script with given user information
+
+*/
+func (g *Gateway) ExecuteAGIScriptAsUser(scriptFile string, targetUser *user.User) (string, error) {
+	//Create a new vm for this request
+	vm := otto.New()
+	//Inject standard libs into the vm
+	g.injectStandardLibs(vm, scriptFile, "")
+	g.injectUserFunctions(vm, targetUser, nil, nil)
+
+	//Try to read the script content
+	scriptContent, err := ioutil.ReadFile(scriptFile)
+	if err != nil {
+		return "", err
+	}
+
+	_, err = vm.Run(scriptContent)
+	if err != nil {
+		return "", err
+	}
+
+	//Get the return valu from the script
+	value, err := vm.Get("HTTP_RESP")
+	if err != nil {
+		return "", err
+	}
+
+	valueString, err := value.ToString()
+	return valueString, nil
+}

+ 204 - 0
mod/agi/agi.http.go

@@ -0,0 +1,204 @@
+package agi
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
+
+	"github.com/robertkrimen/otto"
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	AJGI HTTP Request Library
+
+	This is a library for allowing AGI script to make HTTP Request from the VM
+	Returning either the head or the body of the request
+
+	Author: tobychui
+*/
+
+func (g *Gateway) HTTPLibRegister() {
+	err := g.RegisterLib("http", g.injectHTTPFunctions)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func (g *Gateway) injectHTTPFunctions(vm *otto.Otto, u *user.User) {
+	vm.Set("_http_get", func(call otto.FunctionCall) otto.Value {
+		//Get URL from function variable
+		url, err := call.Argument(0).ToString()
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		//Get respond of the url
+		res, err := http.Get(url)
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		bodyContent, err := ioutil.ReadAll(res.Body)
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		returnValue, err := vm.ToValue(string(bodyContent))
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		return returnValue
+	})
+
+	vm.Set("_http_post", func(call otto.FunctionCall) otto.Value {
+		//Get URL from function paramter
+		url, err := call.Argument(0).ToString()
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		//Get JSON content from 2nd paramter
+		sendWithPayload := true
+		jsonContent, err := call.Argument(1).ToString()
+		if err != nil {
+			//Disable the payload send
+			sendWithPayload = false
+		}
+
+		//Create the request
+		var req *http.Request
+		if sendWithPayload {
+			req, _ = http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonContent)))
+		} else {
+			req, _ = http.NewRequest("POST", url, bytes.NewBuffer([]byte("")))
+		}
+
+		req.Header.Set("Content-Type", "application/json")
+
+		//Send the request
+		client := &http.Client{}
+		resp, err := client.Do(req)
+		if err != nil {
+			log.Println(err)
+			return otto.NaNValue()
+		}
+		defer resp.Body.Close()
+
+		bodyContent, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		returnValue, _ := vm.ToValue(string(bodyContent))
+
+		return returnValue
+	})
+
+	vm.Set("_http_head", func(call otto.FunctionCall) otto.Value {
+		//Get URL from function paramter
+		url, err := call.Argument(0).ToString()
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		//Request the url
+		resp, err := http.Get(url)
+		if err != nil {
+			return otto.NaNValue()
+		}
+
+		headerKey, err := call.Argument(1).ToString()
+		if err != nil || headerKey == "undefined" {
+			//No headkey set. Return the whole header as JSON
+			js, _ := json.Marshal(resp.Header)
+			log.Println(resp.Header)
+			returnValue, _ := vm.ToValue(string(js))
+			return returnValue
+		} else {
+			//headerkey is set. Return if exists
+			possibleValue := resp.Header.Get(headerKey)
+			js, _ := json.Marshal(possibleValue)
+			returnValue, _ := vm.ToValue(string(js))
+			return returnValue
+		}
+
+	})
+
+	vm.Set("_http_download", func(call otto.FunctionCall) otto.Value {
+		//Get URL from function paramter
+		downloadURL, err := call.Argument(0).ToString()
+		if err != nil {
+			return otto.FalseValue()
+		}
+		decodedURL, _ := url.QueryUnescape(downloadURL)
+
+		//Get download desintation from paramter
+		vpath, err := call.Argument(1).ToString()
+		if err != nil {
+			return otto.FalseValue()
+		}
+
+		//Optional: filename paramter
+		filename, err := call.Argument(2).ToString()
+		if err != nil || filename == "undefined" {
+			//Extract the filename from the url instead
+			filename = filepath.Base(decodedURL)
+		}
+
+		//Check user acess permission
+		if !u.CanWrite(vpath) {
+			g.raiseError(errors.New("Permission Denied"))
+			return otto.FalseValue()
+		}
+
+		//Convert the vpath to realpath. Check if it exists
+		rpath, err := u.VirtualPathToRealPath(vpath)
+		if err != nil {
+			return otto.FalseValue()
+		}
+
+		if !fileExists(rpath) || !IsDir(rpath) {
+			g.raiseError(errors.New(vpath + " is a file not a directory."))
+			return otto.FalseValue()
+		}
+
+		downloadDest := filepath.Join(rpath, filename)
+
+		//Ok. Download the file
+		resp, err := http.Get(decodedURL)
+		if err != nil {
+			return otto.FalseValue()
+		}
+		defer resp.Body.Close()
+
+		// Create the file
+		out, err := os.Create(downloadDest)
+		if err != nil {
+			return otto.FalseValue()
+		}
+		defer out.Close()
+
+		// Write the body to file
+		_, err = io.Copy(out, resp.Body)
+		return otto.TrueValue()
+	})
+
+	//Wrap all the native code function into an imagelib class
+	vm.Run(`
+		var http = {};
+		http.get = _http_get;
+		http.post = _http_post;
+		http.head = _http_head;
+		http.download = _http_download;
+	`)
+
+}

+ 267 - 0
mod/agi/agi.image.go

@@ -0,0 +1,267 @@
+package agi
+
+import (
+	"bytes"
+	"errors"
+	"image"
+	"image/jpeg"
+	_ "image/jpeg"
+	"image/png"
+	_ "image/png"
+	"io/ioutil"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/disintegration/imaging"
+	"github.com/oliamb/cutter"
+	"github.com/robertkrimen/otto"
+
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	AJGI Image Processing Library
+
+	This is a library for handling image related functionalities in agi scripts.
+
+*/
+
+func (g *Gateway) ImageLibRegister() {
+	err := g.RegisterLib("imagelib", g.injectImageLibFunctions)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+func (g *Gateway) injectImageLibFunctions(vm *otto.Otto, u *user.User) {
+
+	//Get image dimension, requires filepath (virtual)
+	vm.Set("_imagelib_getImageDimension", func(call otto.FunctionCall) otto.Value {
+		imageFileVpath, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		imagePath, err := virtualPathToRealPath(imageFileVpath, u)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		if !fileExists(imagePath) {
+			g.raiseError(errors.New("File not exists! Given " + imagePath))
+			return otto.FalseValue()
+		}
+
+		file, err := os.Open(imagePath)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		image, _, err := image.DecodeConfig(file)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+		file.Close()
+		rawResults := []int{image.Width, image.Height}
+		result, _ := vm.ToValue(rawResults)
+		return result
+	})
+
+	//Resize image, require (filepath, outputpath, width, height)
+	vm.Set("_imagelib_resizeImage", func(call otto.FunctionCall) otto.Value {
+		vsrc, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		vdest, err := call.Argument(1).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		width, err := call.Argument(2).ToInteger()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		height, err := call.Argument(3).ToInteger()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Convert the virtual paths to real paths
+		rsrc, err := virtualPathToRealPath(vsrc, u)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+		rdest, err := virtualPathToRealPath(vdest, u)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		ext := strings.ToLower(filepath.Ext(rdest))
+		if !inArray([]string{".jpg", ".jpeg", ".png"}, ext) {
+			g.raiseError(errors.New("File extension not supported. Only support .jpg and .png"))
+			return otto.FalseValue()
+		}
+
+		if fileExists(rdest) {
+			err := os.Remove(rdest)
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+		}
+
+		//Resize the image
+		src, err := imaging.Open(rsrc)
+		if err != nil {
+			//Opening failed
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+		src = imaging.Resize(src, int(width), int(height), imaging.Lanczos)
+		err = imaging.Save(src, rdest)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+		return otto.TrueValue()
+	})
+
+	//Crop the given image, require (input, output, posx, posy, width, height)
+	vm.Set("_imagelib_cropImage", func(call otto.FunctionCall) otto.Value {
+		vsrc, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		vdest, err := call.Argument(1).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		posx, err := call.Argument(2).ToInteger()
+		if err != nil {
+			posx = 0
+		}
+
+		posy, err := call.Argument(3).ToInteger()
+		if err != nil {
+			posy = 0
+		}
+
+		width, err := call.Argument(4).ToInteger()
+		if err != nil {
+			g.raiseError(errors.New("Image width not defined"))
+			return otto.FalseValue()
+		}
+
+		height, err := call.Argument(5).ToInteger()
+		if err != nil {
+			g.raiseError(errors.New("Image height not defined"))
+			return otto.FalseValue()
+		}
+
+		//Convert the virtual paths to realpaths
+		rsrc, err := virtualPathToRealPath(vsrc, u)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+		rdest, err := virtualPathToRealPath(vdest, u)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Try to read the source image
+		imageBytes, err := ioutil.ReadFile(rsrc)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		img, _, err := image.Decode(bytes.NewReader(imageBytes))
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Crop the image
+		croppedImg, err := cutter.Crop(img, cutter.Config{
+			Width:  int(width),
+			Height: int(height),
+			Anchor: image.Point{int(posx), int(posy)},
+			Mode:   cutter.TopLeft,
+		})
+
+		//Create the new image
+		out, err := os.Create(rdest)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		if strings.ToLower(filepath.Ext(rdest)) == ".png" {
+			png.Encode(out, croppedImg)
+		} else if strings.ToLower(filepath.Ext(rdest)) == ".jpg" {
+			jpeg.Encode(out, croppedImg, nil)
+		} else {
+			g.raiseError(errors.New("Not supported format: Only support jpg or png"))
+			return otto.FalseValue()
+		}
+
+		out.Close()
+
+		return otto.TrueValue()
+	})
+
+	//Get the given file's thumbnail in base64
+	vm.Set("_imagelib_loadThumbString", func(call otto.FunctionCall) otto.Value {
+		vsrc, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Convert the vsrc to real path
+		rsrc, err := virtualPathToRealPath(vsrc, u)
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Get the files' thumb base64 string
+		base64String, err := g.Option.FileSystemRender.LoadCache(rsrc, false)
+		if err != nil {
+			return otto.FalseValue()
+		} else {
+			value, _ := vm.ToValue(base64String)
+			return value
+		}
+	})
+
+	//Wrap all the native code function into an imagelib class
+	vm.Run(`
+		var imagelib = {};
+		imagelib.getImageDimension = _imagelib_getImageDimension;
+		imagelib.resizeImage = _imagelib_resizeImage;
+		imagelib.cropImage = _imagelib_cropImage;
+		imagelib.loadThumbString = _imagelib_loadThumbString;
+	`)
+}

+ 227 - 0
mod/agi/agi.ws.go

@@ -0,0 +1,227 @@
+package agi
+
+import (
+	"log"
+	"net/http"
+	"sync"
+	"time"
+
+	"github.com/gorilla/websocket"
+	"github.com/robertkrimen/otto"
+	user "imuslab.com/arozos/mod/user"
+)
+
+/*
+	AJGI WebSocket Request Library
+
+	This is a library for allowing AGI based connection upgrade to WebSocket
+	Different from other agi module, this do not use the register lib interface
+	deal to it special nature.
+
+	Author: tobychui
+*/
+var upgrader = websocket.Upgrader{}
+var connections = sync.Map{}
+
+//This is a very special function to check if the connection has been updated or not
+//Return upgrade status (true for already upgraded) and connection uuid
+func checkWebSocketConnectionUpgradeStatus(vm *otto.Otto) (bool, string, *websocket.Conn) {
+	if value, err := vm.Get("_websocket_conn_id"); err == nil {
+		//Exists!
+		//Check if this is undefined
+		if value == otto.UndefinedValue() {
+			//WebSocket connection has closed
+			return false, "", nil
+		}
+
+		//Connection is still live. Try convert it to string
+		connId, err := value.ToString()
+		if err != nil {
+			return false, "", nil
+		}
+
+		//Load the conenction from SyncMap
+		if c, ok := connections.Load(connId); ok {
+			//Return the conncetion object
+			return true, connId, c.(*websocket.Conn)
+		}
+
+		//Connection object not found (Maybe already closed?)
+		return false, "", nil
+
+	}
+	return false, "", nil
+}
+
+func (g *Gateway) injectWebSocketFunctions(vm *otto.Otto, u *user.User, w http.ResponseWriter, r *http.Request) {
+
+	vm.Set("_websocket_upgrade", func(call otto.FunctionCall) otto.Value {
+		//Check if the user specified any timeout time in seconds
+		//Default to 5 minutes
+		timeout, err := call.Argument(0).ToInteger()
+		if err != nil {
+			timeout = 300
+		}
+
+		//Check if the connection has already been updated
+		connState, _, _ := checkWebSocketConnectionUpgradeStatus(vm)
+		if connState {
+			//Already upgraded
+			return otto.TrueValue()
+		}
+
+		//Not upgraded. Upgrade it now
+		c, err := upgrader.Upgrade(w, r, nil)
+		if err != nil {
+			log.Print("*AGI WebSocket*  WebSocket upgrade failed:", err)
+			return otto.FalseValue()
+		}
+
+		//Generate a UUID for this connection
+		connUUID := newUUIDv4()
+		vm.Set("_websocket_conn_id", connUUID)
+		connections.Store(connUUID, c)
+
+		//Record its creation time as opr time
+		vm.Set("_websocket_conn_lastopr", time.Now().Unix())
+
+		//Create a go routine to monitor the connection status and disconnect it if timeup
+		if timeout > 0 {
+			go func() {
+				time.Sleep(1 * time.Second)
+				//Check if the last edit time > timeout time
+				connStatus, connID, conn := checkWebSocketConnectionUpgradeStatus(vm)
+				for connStatus {
+					//For this connection exists
+					if value, err := vm.Get("_websocket_conn_lastopr"); err == nil {
+						lastOprTime, err := value.ToInteger()
+						if err != nil {
+							continue
+						}
+						//log.Println(time.Now().Unix(), lastOprTime)
+						if time.Now().Unix()-lastOprTime > timeout {
+							//Timeout! Kill this socket
+							conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "Timeout"))
+							time.Sleep(300)
+							conn.Close()
+
+							//Clean up the connection in sync map and vm
+							vm.Set("_websocket_conn_id", otto.UndefinedValue())
+							connections.Delete(connID)
+
+							log.Println("*AGI WebSocket* Closing connection due to timeout")
+							break
+						}
+					}
+					time.Sleep(1 * time.Second)
+					connStatus, _, _ = checkWebSocketConnectionUpgradeStatus(vm)
+				}
+
+			}()
+		}
+
+		return otto.TrueValue()
+	})
+
+	vm.Set("_websocket_send", func(call otto.FunctionCall) otto.Value {
+		//Get the content to send
+		content, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Send it
+		connState, connID, conn := checkWebSocketConnectionUpgradeStatus(vm)
+		if !connState {
+			//Already upgraded
+			//log.Println("*AGI WebSocket* Connection id not found in VM")
+			return otto.FalseValue()
+		}
+
+		err = conn.WriteMessage(1, []byte(content))
+		if err != nil {
+
+			//Client connection could have been closed. Close the connection
+			conn.Close()
+
+			//Clean up the connection in sync map and vm
+			vm.Set("_websocket_conn_id", otto.UndefinedValue())
+			connections.Delete(connID)
+			return otto.FalseValue()
+		}
+
+		//Write succeed
+
+		//Update last opr time
+		vm.Set("_websocket_conn_lastopr", time.Now().Unix())
+
+		return otto.TrueValue()
+	})
+
+	vm.Set("_websocket_read", func(call otto.FunctionCall) otto.Value {
+		connState, connID, conn := checkWebSocketConnectionUpgradeStatus(vm)
+		if connState == true {
+			_, message, err := conn.ReadMessage()
+			if err != nil {
+				//Client connection could have been closed. Close the connection
+				conn.Close()
+
+				//Clean up the connection in sync map and vm
+				vm.Set("_websocket_conn_id", otto.UndefinedValue())
+				connections.Delete(connID)
+
+				log.Println("*AGI WebSocket* Trying to read from a closed socket")
+				return otto.FalseValue()
+			}
+			//Update last opr time
+			vm.Set("_websocket_conn_lastopr", time.Now().Unix())
+
+			//Parse the incoming message
+			incomingString, err := otto.ToValue(string(message))
+			if err != nil {
+				log.Println(err)
+				//Unable to parse to JavaScript. Something out of the scope of otto?
+				return otto.NullValue()
+			}
+
+			//Return the incoming string to the AGI script
+			return incomingString
+		} else {
+			//WebSocket not exists
+			//log.Println("*AGI WebSocket* Trying to read from a closed socket")
+			return otto.FalseValue()
+		}
+	})
+
+	vm.Set("_websocket_close", func(call otto.FunctionCall) otto.Value {
+		connState, connID, conn := checkWebSocketConnectionUpgradeStatus(vm)
+		if connState == true {
+			//Close the Websocket gracefully
+			conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
+			time.Sleep(300)
+			conn.Close()
+
+			//Clean up the connection in sync map and vm
+			vm.Set("_websocket_conn_id", otto.UndefinedValue())
+			connections.Delete(connID)
+
+			//Return true value
+			return otto.TrueValue()
+		} else {
+			//Connection not opened or closed already
+			return otto.FalseValue()
+		}
+
+	})
+
+	//Wrap all the native code function into an imagelib class
+	vm.Run(`
+		var websocket = {};
+		websocket.upgrade = _websocket_upgrade;
+		websocket.send = _websocket_send;
+		websocket.read = _websocket_read;
+		websocket.close = _websocket_close;
+		
+	`)
+}

+ 178 - 0
mod/agi/common.go

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

+ 34 - 0
mod/agi/error.go

@@ -0,0 +1,34 @@
+package agi
+
+import (
+	"net/http"
+	"io/ioutil"
+	"strconv"
+	"time"
+
+	"github.com/valyala/fasttemplate"
+)
+
+/*
+	Error Template Rendering for AGI script error
+
+	This script is used to handle a PHP-like error message for the user
+	For any runtime error, please see the console for more information.
+*/
+
+
+func (g *Gateway)RenderErrorTemplate(w http.ResponseWriter, errmsg string, scriptpath string){
+	template, _ := ioutil.ReadFile("system/agi/error.html")
+	t := fasttemplate.New(string(template), "{{", "}}")
+	s := t.ExecuteString(map[string]interface{}{
+		"error_msg":  errmsg,
+		"script_filepath":  scriptpath,
+		"timestamp":  strconv.Itoa(int(time.Now().Unix())),
+		"major_version":  g.Option.BuildVersion,
+		"minor_version":  g.Option.InternalVersion,
+		"agi_version":  AgiVersion,
+	})
+	w.WriteHeader(http.StatusInternalServerError)
+	w.Write([]byte(s))
+}
+

+ 56 - 0
mod/agi/static.go

@@ -0,0 +1,56 @@
+package agi
+
+import (
+	"net/url"
+	"path/filepath"
+	"strings"
+)
+
+//Return the script root of the current executing script
+func getScriptRoot(scriptFile string, scriptScope string) string {
+	//Get the script root from the script path
+	webRootAbs, _ := filepath.Abs(scriptScope)
+	webRootAbs = filepath.ToSlash(filepath.Clean(webRootAbs) + "/")
+	scriptFileAbs, _ := filepath.Abs(scriptFile)
+	scriptFileAbs = filepath.ToSlash(filepath.Clean(scriptFileAbs))
+	scriptRoot := strings.Replace(scriptFileAbs, webRootAbs, "", 1)
+	scriptRoot = strings.Split(scriptRoot, "/")[0]
+	return scriptRoot
+}
+
+//For handling special url decode in the request
+func specialURIDecode(inputPath string) string {
+	inputPath = strings.ReplaceAll(inputPath, "+", "{{plus_sign}}")
+	inputPath, _ = url.QueryUnescape(inputPath)
+	inputPath = strings.ReplaceAll(inputPath, "{{plus_sign}}", "+")
+	return inputPath
+}
+
+func specialGlob(path string) ([]string, error) {
+	files, err := filepath.Glob(path)
+	if err != nil {
+		return []string{}, err
+	}
+
+	if strings.Contains(path, "[") == true || strings.Contains(path, "]") == true {
+		if len(files) == 0 {
+			//Handle reverse check. Replace all [ and ] with *
+			newSearchPath := strings.ReplaceAll(path, "[", "?")
+			newSearchPath = strings.ReplaceAll(newSearchPath, "]", "?")
+			newSearchPath = strings.ReplaceAll(newSearchPath, ":", "?")
+			//Scan with all the similar structure except [ and ]
+			tmpFilelist, _ := filepath.Glob(newSearchPath)
+			for _, file := range tmpFilelist {
+				file = filepath.ToSlash(file)
+				if strings.Contains(file, filepath.ToSlash(filepath.Dir(path))) {
+					files = append(files, file)
+				}
+			}
+		}
+	}
+	//Convert all filepaths to slash
+	for i := 0; i < len(files); i++ {
+		files[i] = filepath.ToSlash(files[i])
+	}
+	return files, nil
+}

+ 352 - 0
mod/agi/systemFunc.go

@@ -0,0 +1,352 @@
+package agi
+
+import (
+	"encoding/csv"
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"log"
+	"os/exec"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/robertkrimen/otto"
+)
+
+//Inject aroz online custom functions into the virtual machine
+func (g *Gateway) injectStandardLibs(vm *otto.Otto, scriptFile string, scriptScope string) {
+	//Define system core modules and definations
+	sysdb := g.Option.UserHandler.GetDatabase()
+
+	//Define VM global variables
+	vm.Set("BUILD_VERSION", g.Option.BuildVersion)
+	vm.Set("INTERNVAL_VERSION", g.Option.InternalVersion)
+	vm.Set("LOADED_MODULES", g.Option.LoadedModule)
+	vm.Set("LOADED_STORAGES", g.Option.UserHandler.GetStoragePool())
+	vm.Set("HTTP_RESP", "")
+	vm.Set("HTTP_HEADER", "text/plain")
+
+	//Response related
+	vm.Set("sendResp", func(call otto.FunctionCall) otto.Value {
+		argString, _ := call.Argument(0).ToString()
+		vm.Set("HTTP_RESP", argString)
+		return otto.Value{}
+	})
+
+	vm.Set("sendJSONResp", func(call otto.FunctionCall) otto.Value {
+		argString, _ := call.Argument(0).ToString()
+		vm.Set("HTTP_HEADER", "application/json")
+		vm.Set("HTTP_RESP", argString)
+		return otto.Value{}
+	})
+
+	//Database related
+	//newDBTableIfNotExists(tableName)
+	vm.Set("newDBTableIfNotExists", func(call otto.FunctionCall) otto.Value {
+		tableName, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		//Create the table with given tableName
+		if g.filterDBTable(tableName, false) {
+			sysdb.NewTable(tableName)
+			//Return true
+			reply, _ := vm.ToValue(true)
+			return reply
+		}
+		reply, _ := vm.ToValue(false)
+		return reply
+	})
+
+	vm.Set("DBTableExists", func(call otto.FunctionCall) otto.Value {
+		tableName, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		//Create the table with given tableName
+		if sysdb.TableExists(tableName) {
+			return otto.TrueValue()
+		}
+
+		return otto.FalseValue()
+	})
+
+	//dropDBTable(tablename)
+	vm.Set("dropDBTable", func(call otto.FunctionCall) otto.Value {
+		tableName, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		//Create the table with given tableName
+		if g.filterDBTable(tableName, true) {
+			sysdb.DropTable(tableName)
+			reply, _ := vm.ToValue(true)
+			return reply
+		}
+
+		//Return true
+		reply, _ := vm.ToValue(false)
+		return reply
+	})
+
+	//writeDBItem(tablename, key, value) => return true when suceed
+	vm.Set("writeDBItem", func(call otto.FunctionCall) otto.Value {
+		tableName, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+
+		//Check if the tablename is reserved
+		if g.filterDBTable(tableName, true) {
+			keyString, err := call.Argument(1).ToString()
+			if err != nil {
+				g.raiseError(err)
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+			valueString, err := call.Argument(2).ToString()
+			if err != nil {
+				g.raiseError(err)
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+			sysdb.Write(tableName, keyString, valueString)
+			reply, _ := vm.ToValue(true)
+			return reply
+		}
+
+		reply, _ := vm.ToValue(false)
+		return reply
+
+	})
+
+	//readDBItem(tablename, key) => return value
+	vm.Set("readDBItem", func(call otto.FunctionCall) otto.Value {
+		tableName, _ := call.Argument(0).ToString()
+		keyString, _ := call.Argument(1).ToString()
+		returnValue := ""
+		reply, _ := vm.ToValue(nil)
+		if g.filterDBTable(tableName, true) {
+			sysdb.Read(tableName, keyString, &returnValue)
+			r, _ := vm.ToValue(returnValue)
+			reply = r
+		} else {
+			reply = otto.FalseValue()
+		}
+		return reply
+	})
+
+	//listDBTable(tablename) => Return key values array
+	vm.Set("listDBTable", func(call otto.FunctionCall) otto.Value {
+		tableName, _ := call.Argument(0).ToString()
+		returnValue := map[string]string{}
+		reply, _ := vm.ToValue(nil)
+		if g.filterDBTable(tableName, true) {
+			entries, _ := sysdb.ListTable(tableName)
+			for _, keypairs := range entries {
+				//Decode the string
+				result := ""
+				json.Unmarshal(keypairs[1], &result)
+				returnValue[string(keypairs[0])] = result
+			}
+			r, err := vm.ToValue(returnValue)
+			if err != nil {
+				return otto.NullValue()
+			}
+			return r
+		} else {
+			reply = otto.FalseValue()
+		}
+		return reply
+	})
+
+	//deleteDBItem(tablename, key) => Return true if success, false if failed
+	vm.Set("deleteDBItem", func(call otto.FunctionCall) otto.Value {
+		tableName, _ := call.Argument(0).ToString()
+		keyString, _ := call.Argument(1).ToString()
+		if g.filterDBTable(tableName, true) {
+			err := sysdb.Delete(tableName, keyString)
+			if err != nil {
+				return otto.FalseValue()
+			}
+		} else {
+			//Permission denied
+			return otto.FalseValue()
+		}
+
+		return otto.TrueValue()
+	})
+
+	//Module registry
+	vm.Set("registerModule", func(call otto.FunctionCall) otto.Value {
+		jsonModuleConfig, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		//Try to decode it to a module Info
+		g.Option.ModuleRegisterParser(jsonModuleConfig)
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(false)
+			return reply
+		}
+		return otto.Value{}
+	})
+
+	//Package Executation. Only usable when called to a given script File.
+	if scriptFile != "" && scriptScope != "" {
+		//Package request --> Install linux package if not exists
+		vm.Set("requirepkg", func(call otto.FunctionCall) otto.Value {
+			packageName, err := call.Argument(0).ToString()
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+			requireComply, err := call.Argument(1).ToBoolean()
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			scriptRoot := getScriptRoot(scriptFile, scriptScope)
+
+			//Check if this module already get registered.
+			alreadyRegistered := false
+			for _, pkgRequest := range g.AllowAccessPkgs[strings.ToLower(packageName)] {
+				if pkgRequest.InitRoot == scriptRoot {
+					alreadyRegistered = true
+					break
+				}
+			}
+
+			if !alreadyRegistered {
+				//Register this packge to this script and allow the module to call this package
+				g.AllowAccessPkgs[strings.ToLower(packageName)] = append(g.AllowAccessPkgs[strings.ToLower(packageName)], AgiPackage{
+					InitRoot: scriptRoot,
+				})
+			}
+
+			//Try to install the package via apt
+			err = g.Option.PackageManager.InstallIfNotExists(packageName, requireComply)
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			return otto.TrueValue()
+		})
+
+		//Exec required pkg with permission control
+		vm.Set("execpkg", func(call otto.FunctionCall) otto.Value {
+			//Check if the pkg is already registered
+			scriptRoot := getScriptRoot(scriptFile, scriptScope)
+			packageName, err := call.Argument(0).ToString()
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			if val, ok := g.AllowAccessPkgs[packageName]; ok {
+				//Package already registered by at least one module. Check if this script root registered
+				thisModuleRegistered := false
+				for _, registeredPkgInterface := range val {
+					if registeredPkgInterface.InitRoot == scriptRoot {
+						//This package registered this command. Allow access
+						thisModuleRegistered = true
+					}
+				}
+
+				if !thisModuleRegistered {
+					g.raiseError(errors.New("Package request not registered: " + packageName))
+					return otto.FalseValue()
+				}
+
+			} else {
+				g.raiseError(errors.New("Package request not registered: " + packageName))
+				return otto.FalseValue()
+			}
+
+			//Ok. Allow paramter to be loaded
+			execParamters, _ := call.Argument(1).ToString()
+
+			// Split input paramters into []string
+			r := csv.NewReader(strings.NewReader(execParamters))
+			r.Comma = ' ' // space
+			fields, err := r.Read()
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			//Run os.Exec on the given commands
+			cmd := exec.Command(packageName, fields...)
+			out, err := cmd.CombinedOutput()
+			if err != nil {
+				log.Println(string(out))
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			reply, _ := vm.ToValue(string(out))
+			return reply
+		})
+
+		//Include another js in runtime
+		vm.Set("includes", func(call otto.FunctionCall) otto.Value {
+			//Check if the pkg is already registered
+			scriptName, err := call.Argument(0).ToString()
+			if err != nil {
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			//Check if it is calling itself
+			if filepath.Base(scriptFile) == filepath.Base(scriptName) {
+				g.raiseError(errors.New("*AGI* Self calling is not allowed"))
+				return otto.FalseValue()
+			}
+
+			//Check if the script file exists
+			targetScriptPath := filepath.ToSlash(filepath.Join(filepath.Dir(scriptFile), scriptName))
+			if !fileExists(targetScriptPath) {
+				g.raiseError(errors.New("*AGI* Target path not exists!"))
+				return otto.FalseValue()
+			}
+
+			//Run the script
+			scriptContent, _ := ioutil.ReadFile(targetScriptPath)
+			_, err = vm.Run(string(scriptContent))
+			if err != nil {
+				//Script execution failed
+				log.Println("Script Execution Failed: ", err.Error())
+				g.raiseError(err)
+				return otto.FalseValue()
+			}
+
+			return otto.TrueValue()
+		})
+
+	}
+
+	//Delay, sleep given ms
+	vm.Set("delay", func(call otto.FunctionCall) otto.Value {
+		delayTime, err := call.Argument(0).ToInteger()
+		if err != nil {
+			g.raiseError(err)
+			return otto.FalseValue()
+		}
+		time.Sleep(time.Duration(delayTime) * time.Millisecond)
+		return otto.TrueValue()
+	})
+}

+ 276 - 0
mod/agi/userFunc.go

@@ -0,0 +1,276 @@
+package agi
+
+import (
+	"encoding/json"
+	"errors"
+	"log"
+	"net/http"
+	"path/filepath"
+
+	"github.com/robertkrimen/otto"
+	user "imuslab.com/arozos/mod/user"
+)
+
+//Define path translation function
+func virtualPathToRealPath(path string, u *user.User) (string, error) {
+	return u.VirtualPathToRealPath(path)
+}
+
+func realpathToVirtualpath(path string, u *user.User) (string, error) {
+	return u.RealPathToVirtualPath(path)
+}
+
+//Inject user based functions into the virtual machine
+func (g *Gateway) injectUserFunctions(vm *otto.Otto, u *user.User, w http.ResponseWriter, r *http.Request) {
+	username := u.Username
+	vm.Set("USERNAME", username)
+	vm.Set("USERICON", u.GetUserIcon())
+	vm.Set("USERQUOTA_TOTAL", u.StorageQuota.TotalStorageQuota)
+	vm.Set("USERQUOTA_USED", u.StorageQuota.UsedStorageQuota)
+	vm.Set("USER_VROOTS", u.GetAllFileSystemHandler())
+	vm.Set("USER_MODULES", u.GetUserAccessibleModules())
+
+	//File system and path related
+	vm.Set("decodeVirtualPath", func(call otto.FunctionCall) otto.Value {
+		path, _ := call.Argument(0).ToString()
+		realpath, err := virtualPathToRealPath(path, u)
+		if err != nil {
+			reply, _ := vm.ToValue(false)
+			return reply
+		} else {
+			reply, _ := vm.ToValue(realpath)
+			return reply
+		}
+	})
+
+	vm.Set("decodeAbsoluteVirtualPath", func(call otto.FunctionCall) otto.Value {
+		path, _ := call.Argument(0).ToString()
+		realpath, err := virtualPathToRealPath(path, u)
+		if err != nil {
+			reply, _ := vm.ToValue(false)
+			return reply
+		} else {
+			//Convert the real path to absolute path
+			abspath, err := filepath.Abs(realpath)
+			if err != nil {
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+			reply, _ := vm.ToValue(abspath)
+			return reply
+		}
+	})
+
+	vm.Set("encodeRealPath", func(call otto.FunctionCall) otto.Value {
+		path, _ := call.Argument(0).ToString()
+		realpath, err := realpathToVirtualpath(path, u)
+		if err != nil {
+			reply, _ := vm.ToValue(false)
+			return reply
+		} else {
+			reply, _ := vm.ToValue(realpath)
+			return reply
+		}
+	})
+
+	//Check if a given virtual path is readonly
+	vm.Set("pathCanWrite", func(call otto.FunctionCall) otto.Value {
+		vpath, _ := call.Argument(0).ToString()
+		if u.CanWrite(vpath) {
+			return otto.TrueValue()
+		} else {
+			return otto.FalseValue()
+		}
+	})
+
+	//Permission related
+	vm.Set("getUserPermissionGroup", func(call otto.FunctionCall) otto.Value {
+		groupinfo := u.GetUserPermissionGroup()
+		jsonString, _ := json.Marshal(groupinfo)
+		reply, _ := vm.ToValue(string(jsonString))
+		return reply
+	})
+
+	vm.Set("userIsAdmin", func(call otto.FunctionCall) otto.Value {
+		reply, _ := vm.ToValue(u.IsAdmin())
+		return reply
+	})
+
+	//User Account Related
+	/*
+		userExists(username);
+	*/
+	vm.Set("userExists", func(call otto.FunctionCall) otto.Value {
+		if u.IsAdmin() {
+			//Get username from function paramter
+			username, err := call.Argument(0).ToString()
+			if err != nil || username == "undefined" {
+				g.raiseError(errors.New("username is undefined"))
+				reply, _ := vm.ToValue(nil)
+				return reply
+			}
+
+			//Check if user exists
+			userExists := u.Parent().GetAuthAgent().UserExists(username)
+			if userExists {
+				return otto.TrueValue()
+			} else {
+				return otto.FalseValue()
+			}
+
+		} else {
+			g.raiseError(errors.New("Permission Denied: userExists require admin permission"))
+			return otto.FalseValue()
+		}
+
+	})
+
+	/*
+		createUser(username, password, defaultGroup);
+	*/
+	vm.Set("createUser", func(call otto.FunctionCall) otto.Value {
+		if u.IsAdmin() {
+			//Ok. Create user base on given information
+			username, err := call.Argument(0).ToString()
+			if err != nil || username == "undefined" {
+				g.raiseError(errors.New("username is undefined"))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			password, err := call.Argument(1).ToString()
+			if err != nil || password == "undefined" {
+				g.raiseError(errors.New("password is undefined"))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			defaultGroup, err := call.Argument(2).ToString()
+			if err != nil || defaultGroup == "undefined" {
+				g.raiseError(errors.New("defaultGroup is undefined"))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			//Check if username already used
+			userExists := u.Parent().GetAuthAgent().UserExists(username)
+			if userExists {
+				g.raiseError(errors.New("Username already exists"))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			//Check if the given permission group exists
+			groupExists := u.Parent().GetPermissionHandler().GroupExists(defaultGroup)
+			if !groupExists {
+				g.raiseError(errors.New(defaultGroup + " user-group not exists"))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			//Create the user
+			err = u.Parent().GetAuthAgent().CreateUserAccount(username, password, []string{defaultGroup})
+
+			if err != nil {
+				g.raiseError(errors.New("User creation failed: " + err.Error()))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			return otto.TrueValue()
+		} else {
+			g.raiseError(errors.New("Permission Denied: createUser require admin permission"))
+			return otto.FalseValue()
+		}
+
+	})
+
+	vm.Set("editUser", func(call otto.FunctionCall) otto.Value {
+		if u.IsAdmin() {
+
+		} else {
+			g.raiseError(errors.New("Permission Denied: editUser require admin permission"))
+			return otto.FalseValue()
+		}
+		//libname, err := call.Argument(0).ToString()
+		return otto.FalseValue()
+	})
+
+	/*
+		removeUser(username)
+	*/
+	vm.Set("removeUser", func(call otto.FunctionCall) otto.Value {
+		if u.IsAdmin() {
+			//Get username from function paramters
+			username, err := call.Argument(0).ToString()
+			if err != nil || username == "undefined" {
+				g.raiseError(errors.New("username is undefined"))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			//Check if the user exists
+			userExists := u.Parent().GetAuthAgent().UserExists(username)
+			if !userExists {
+				g.raiseError(errors.New(username + " not exists"))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			//User exists. Remove it from the system
+			err = u.Parent().GetAuthAgent().UnregisterUser(username)
+			if err != nil {
+				g.raiseError(errors.New("User removal failed: " + err.Error()))
+				reply, _ := vm.ToValue(false)
+				return reply
+			}
+
+			return otto.TrueValue()
+		} else {
+			g.raiseError(errors.New("Permission Denied: removeUser require admin permission"))
+			return otto.FalseValue()
+		}
+	})
+
+	vm.Set("getUserInfoByName", func(call otto.FunctionCall) otto.Value {
+		//libname, err := call.Argument(0).ToString()
+		if u.IsAdmin() {
+
+		} else {
+
+			g.raiseError(errors.New("Permission Denied: getUserInfoByName require admin permission"))
+			return otto.FalseValue()
+		}
+		return otto.TrueValue()
+	})
+
+	//Allow real time library includsion into the virtual machine
+	vm.Set("requirelib", func(call otto.FunctionCall) otto.Value {
+		libname, err := call.Argument(0).ToString()
+		if err != nil {
+			g.raiseError(err)
+			reply, _ := vm.ToValue(nil)
+			return reply
+		}
+
+		//Handle special case on high level libraries
+		if libname == "websocket" && w != nil && r != nil {
+			g.injectWebSocketFunctions(vm, u, w, r)
+			return otto.TrueValue()
+		} else {
+			//Check if the library name exists. If yes, run the initiation script on the vm
+			if entryPoint, ok := g.LoadedAGILibrary[libname]; ok {
+				entryPoint(vm, u)
+				return otto.TrueValue()
+			} else {
+				//Lib not exists
+				log.Println("Lib not found: " + libname)
+				return otto.FalseValue()
+			}
+		}
+
+		//Unknown status
+		return otto.FalseValue()
+	})
+
+}

+ 132 - 0
mod/apt/apt.go

@@ -0,0 +1,132 @@
+package apt
+
+import (
+	"encoding/json"
+	"errors"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"runtime"
+	"strings"
+)
+
+/*
+	Pacakge management tool for Linux OS with APT
+
+	ONLY USABLE under Linux environment
+*/
+
+type AptPackageManager struct {
+	AllowAutoInstall bool
+}
+
+func NewPackageManager(autoInstall bool) *AptPackageManager {
+	return &AptPackageManager{
+		AllowAutoInstall: autoInstall,
+	}
+}
+
+//Install the given package if not exists. Set mustComply to true for "panic on failed to install"
+func (a *AptPackageManager) InstallIfNotExists(pkgname string, mustComply bool) error {
+	//Clear the pkgname
+	pkgname = strings.ReplaceAll(pkgname, "&", "")
+	pkgname = strings.ReplaceAll(pkgname, "|", "")
+
+	if runtime.GOOS == "windows" {
+		//Check if the command already exists in windows path paramters.
+		cmd := exec.Command("where", pkgname, "2>", "nul")
+		_, err := cmd.CombinedOutput()
+		if err != nil {
+			return errors.New("Package " + pkgname + " not found in Windows %PATH%.")
+		}
+		return nil
+	} else if runtime.GOOS == "darwin" {
+		//Mac OS. Check if package exists
+		cmd := exec.Command("whereis", pkgname)
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			return errors.New("Package " + pkgname + " not found in MacOS ENV variable.")
+		}
+
+		if strings.TrimSpace(string(out)) == "" {
+			//Package not exists
+			return errors.New("Package " + pkgname + " not installed on this Mac")
+		}
+		return nil
+	}
+
+	if a.AllowAutoInstall == false {
+		return errors.New("Package auto install is disabled")
+	}
+
+	cmd := exec.Command("which", pkgname)
+	out, _ := cmd.CombinedOutput()
+
+	//log.Println(packageInfo)
+	if len(string(out)) > 1 {
+		return nil
+	} else {
+		//Package not installed. Install if now if running in sudo mode
+		log.Println("Installing package " + pkgname + "...")
+		cmd := exec.Command("apt-get", "install", "-y", pkgname)
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		err := cmd.Run()
+		if err != nil {
+			if mustComply {
+				//Panic and terminate server process
+				log.Println("Installation failed on package: "+pkgname, string(out))
+				os.Exit(0)
+			} else {
+				log.Println("Installation failed on package: " + pkgname)
+				log.Println(string(out))
+			}
+			return err
+		}
+		return nil
+	}
+
+	return nil
+}
+
+func HandlePackageListRequest(w http.ResponseWriter, r *http.Request) {
+	if runtime.GOOS == "windows" {
+		w.Header().Set("Content-Type", "application/json")
+		w.Write([]byte("{\"error\":\"" + "Function disabled on Windows" + "\"}"))
+		return
+	}
+	cmd := exec.Command("apt", "list", "--installed")
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		w.Header().Set("Content-Type", "application/json")
+		w.Write([]byte("{\"error\":\"" + err.Error() + "\"}"))
+		return
+	}
+
+	results := [][]string{}
+	//Parse the output string
+	installedPackages := strings.Split(string(out), "\n")
+	for _, thisPackage := range installedPackages {
+		if len(thisPackage) > 0 {
+			packageInfo := strings.Split(thisPackage, "/")
+			packageName := packageInfo[0]
+			if len(packageInfo) >= 2 {
+				packageVersion := strings.Split(packageInfo[1], ",")[1]
+				if packageVersion[:3] == "now" {
+					packageVersion = packageVersion[4:]
+				}
+				if strings.Contains(packageVersion, "[installed") && packageVersion[len(packageVersion)-1:] != "]" {
+					packageVersion = packageVersion + ",automatic]"
+				}
+
+				results = append(results, []string{packageName, packageVersion})
+			}
+		}
+	}
+
+	jsonString, _ := json.Marshal(results)
+	w.Header().Set("Content-Type", "application/json")
+	w.Write(jsonString)
+	return
+}

+ 12 - 0
mod/arsm/README.md

@@ -0,0 +1,12 @@
+# Arozos Remote Support & Management Module
+
+
+## Introduction
+Arozos Remote Support & Management Module or ARSM module, is a module that handles remote maintaince and support
+for downstream arozos powered system on client that is located under NAT routers
+
+## Usage
+This is not a module that can be directly acceessed. See the submodule under this module for more information
+and documentation.
+
+s

+ 241 - 0
mod/arsm/aecron/aecron.go

@@ -0,0 +1,241 @@
+package aecron
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"log"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+
+	"imuslab.com/arozos/mod/agi"
+	"imuslab.com/arozos/mod/user"
+)
+
+/*
+	ArOZ Emulated Crontab
+	author: tobychui
+
+	This is not actually a crontab but something similar that provide
+	timered operations for executing commands in agi or bash in an interval
+	bases
+
+*/
+
+type Job struct {
+	Name              string //The name of this job
+	Creator           string //The creator of this job. When execute, this user permission will be used
+	Description       string //Job description, can be empty
+	Admin             bool   //If the creator has admin permission during the creation of this job. If this doesn't match with the runtime instance, this job wille be skipped
+	ExecutionInterval int64  //Execuation interval in seconds
+	BaseTime          int64  //Exeuction basetime. The next interval is calculated using (current time - base time ) % execution interval
+	ScriptFile        string //The script file being called. Can be an agi script (.agi / .js) or shell script (.bat or .sh)
+}
+
+type Aecron struct {
+	jobs        []*Job
+	cronfile    string
+	userHandler *user.UserHandler
+	gateway     *agi.Gateway
+	ticker      chan bool
+}
+
+var (
+	logFolder string = "./system/aecron/"
+)
+
+func NewArozEmulatedCrontab(userHandler *user.UserHandler, gateway *agi.Gateway, cronfile string) (*Aecron, error) {
+	if !fileExists(cronfile) {
+		//Cronfile not exists. Create it
+		emptyJobList := []*Job{}
+		ls, _ := json.Marshal(emptyJobList)
+		err := ioutil.WriteFile(cronfile, ls, 0755)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	//Load previous jobs from file
+	jobs, err := loadJobsFromFile(cronfile)
+	if err != nil {
+		return nil, err
+	}
+
+	//Create the ArOZ Emulated Crontask
+	aecron := Aecron{
+		jobs:        jobs,
+		userHandler: userHandler,
+		gateway:     gateway,
+		cronfile:    cronfile,
+	}
+
+	//Create log folder
+	os.MkdirAll(logFolder, 0755)
+
+	//Start the cronjob at 1 minute ticker interval
+	go func() {
+		//Delay start: Wait until seconds = 0
+		for time.Now().Unix()%60 > 0 {
+			time.Sleep(500 * time.Millisecond)
+		}
+		stopChannel := aecron.createTicker(1 * time.Minute)
+		aecron.ticker = stopChannel
+		log.Println("Emulated Crontab Started - Scheduling Tasks")
+	}()
+
+	//Return the crontask
+	return &aecron, nil
+}
+
+//Load a list of jobs from file
+func loadJobsFromFile(cronfile string) ([]*Job, error) {
+	//Try to read the cronfile
+	filecontent, err := ioutil.ReadFile(cronfile)
+	if err != nil {
+		return []*Job{}, err
+	}
+
+	//Phrase the cronfile
+	prevousJobs := []Job{}
+	err = json.Unmarshal(filecontent, &prevousJobs)
+	if err != nil {
+		return []*Job{}, err
+	}
+
+	//Convert the json objets to pointer for easy changing by other process
+	jobsPointers := []*Job{}
+	for _, thisJob := range prevousJobs {
+		var newJobPointer Job = thisJob
+		jobsPointers = append(jobsPointers, &newJobPointer)
+	}
+
+	return jobsPointers, nil
+}
+
+func (a *Aecron) createTicker(duration time.Duration) chan bool {
+	ticker := time.NewTicker(duration)
+	stop := make(chan bool, 1)
+
+	go func() {
+		defer log.Println("Aecron Stopped")
+		for {
+			select {
+			case <-ticker.C:
+				//Run jobs
+				for _, thisJob := range a.jobs {
+					if (time.Now().Unix()-thisJob.BaseTime)%thisJob.ExecutionInterval == 0 {
+						//Execute this job
+						scriptFile := thisJob.ScriptFile
+						if !fileExists(scriptFile) {
+							//This job no longer exists in the file system. Remove it
+							a.RemoveJobFromScheduleList(thisJob.Name)
+						}
+						clonedJobStructure := *thisJob
+						ext := filepath.Ext(scriptFile)
+						if ext == ".js" || ext == ".agi" {
+							//Run using AGI interface in go routine
+							go func(thisJob Job) {
+								userinfo, err := a.userHandler.GetUserInfoFromUsername(thisJob.Creator)
+								if err != nil {
+									//This user not exists. Skip this script
+									cronlog("[ERROR] User not exists: " + thisJob.Creator + ". Skipping scheduled job: " + thisJob.Name + ".")
+									return
+								}
+
+								//Run the script with this user scope
+								resp, err := a.gateway.ExecuteAGIScriptAsUser(thisJob.ScriptFile, userinfo)
+								if err != nil {
+									cronlog("[ERROR] " + thisJob.Name + " " + err.Error())
+								} else {
+									cronlog(thisJob.Name + " " + resp)
+								}
+							}(clonedJobStructure)
+
+						} else if ext == ".bat" || ext == ".sh" {
+							//Run as shell script
+							go func(thisJob Job) {
+								scriptPath := thisJob.ScriptFile
+								if runtime.GOOS == "windows" {
+									scriptPath = strings.ReplaceAll(filepath.ToSlash(scriptPath), "/", "\\")
+								}
+								cmd := exec.Command(scriptPath)
+								out, err := cmd.CombinedOutput()
+								if err != nil {
+									cronlog("[ERROR] " + thisJob.Name + " " + err.Error() + " => " + string(out))
+								}
+								cronlog(thisJob.Name + " " + string(out))
+							}(clonedJobStructure)
+						} else {
+							//Unknown script file. Ignore this
+							log.Println("This extension is not yet supported: ", ext)
+						}
+					}
+				}
+			case <-stop:
+				return
+			}
+		}
+	}()
+
+	return stop
+}
+
+func (a *Aecron) Close() {
+	if a.ticker != nil {
+		//Stop the ticker
+		a.ticker <- true
+	}
+}
+
+func (a *Aecron) GetScheduledJobByName(name string) *Job {
+	for _, thisJob := range a.jobs {
+		if thisJob.Name == name {
+			return thisJob
+		}
+	}
+
+	return nil
+}
+
+func (a *Aecron) RemoveJobFromScheduleList(taskName string) {
+	newJobSlice := []*Job{}
+	for _, j := range a.jobs {
+		if j.Name != taskName {
+			thisJob := j
+			newJobSlice = append(newJobSlice, thisJob)
+		}
+	}
+	a.jobs = newJobSlice
+}
+
+func (a *Aecron) JobExists(name string) bool {
+	targetJob := a.GetScheduledJobByName(name)
+	if targetJob == nil {
+		return false
+	} else {
+		return true
+	}
+}
+
+//Write the output to log file. Default to ./system/aecron/{date}.log
+func cronlog(message string) {
+	currentTime := time.Now()
+	timestamp := currentTime.Format("2006-01-02 15:04:05")
+	message = timestamp + " " + message
+	currentLogFile := filepath.ToSlash(filepath.Clean(logFolder)) + "/" + time.Now().Format("01-02-2006") + ".log"
+	f, err := os.OpenFile(currentLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+	if err != nil {
+		//Unable to write to file. Log to STDOUT instead
+		log.Println(message)
+		return
+	}
+	if _, err := f.WriteString(message + "\n"); err != nil {
+		log.Println(message)
+		return
+	}
+	defer f.Close()
+
+}

Some files were not shown because too many files changed in this diff