Bladeren bron

Added wip background task system, DO NOT BUILD

TC pushbot 5 2 jaren geleden
bovenliggende
commit
a004640b36

+ 204 - 7
file_system.go

@@ -13,6 +13,7 @@ import (
 	"net/http"
 	"net/url"
 	"os"
+	"sync"
 
 	"path/filepath"
 	"runtime"
@@ -46,6 +47,7 @@ var (
 	thumbRenderHandler *metadata.RenderHandler
 	shareEntryTable    *shareEntry.ShareEntryTable
 	shareManager       *share.Manager
+	wsConnectionStore  sync.Map
 )
 
 type trashedFile struct {
@@ -60,6 +62,16 @@ type trashedFile struct {
 	OriginalFilename string
 }
 
+type fileOperationTask struct {
+	ID                  string  //Unique id for the task operation
+	Owner               string  //Owner of the file opr
+	Src                 string  //Source folder for opr
+	Dest                string  //Destination folder for opr
+	Progress            float64 //Progress for the operation
+	LatestFile          string  //Latest file that is current transfering
+	FileOperationSignal int     //Current control signal of the file opr
+}
+
 func FileSystemInit() {
 	router := prout.NewModuleRouter(prout.RouterOption{
 		ModuleName:  "File Manager",
@@ -195,6 +207,13 @@ func FileSystemInit() {
 	//Share function is now routed by the main router
 	//http.HandleFunc("/share", shareManager.HandleShareAccess)
 
+	/*
+		File Operation Resume Functions
+	*/
+	//Create a sync map for file operation opened connections
+	wsConnectionStore = sync.Map{}
+	router.HandleFunc("/system/file_system/ongoing", system_fs_HandleOnGoingTasks)
+
 	/*
 		Nighly Tasks
 
@@ -1372,7 +1391,7 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 	if operation == "move" || operation == "copy" || operation == "zip" || operation == "unzip" {
 
 	} else {
-		systemWideLogger.PrintAndLog("File System", "This file operation is not supported on WebSocket file operations endpoint. Please use the legacy endpoint instead. Received: "+operation, errors.New("operaiton not supported on websocket endpoint"))
+		systemWideLogger.PrintAndLog("File System", "This file operation is not supported on WebSocket file operations endpoint. Please use the POST request endpoint instead. Received: "+operation, errors.New("operaiton not supported on websocket endpoint"))
 		w.WriteHeader(http.StatusInternalServerError)
 		w.Write([]byte("500 - Not supported operation"))
 		return
@@ -1389,9 +1408,26 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
+	//Create the file operation task and remember it
+	oprId := strconv.Itoa(int(time.Now().Unix())) + "_" + uuid.NewV4().String()
+	thisFileOperationTask := fileOperationTask{
+		ID:         oprId,
+		Owner:      userinfo.Username,
+		Src:        arozfs.ToSlash(filepath.Dir(sourceFiles[0])),
+		Dest:       arozfs.ToSlash(vdestFile),
+		Progress:   0.0,
+		LatestFile: arozfs.ToSlash(filepath.Base(sourceFiles[0])),
+	}
+	wsConnectionStore.Store(oprId, &thisFileOperationTask)
+
+	//Send over the oprId for this file operation for tracking
+	time.Sleep(300 * time.Millisecond)
+	c.WriteMessage(1, []byte("{\"oprid\":\""+oprId+"\"}"))
+
 	type ProgressUpdate struct {
 		LatestFile string
 		Progress   int
+		StatusFlag int
 		Error      string
 	}
 
@@ -1413,10 +1449,13 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 					LatestFile: filepath.Base(vsrcs),
 					Progress:   -1,
 					Error:      "File not exists",
+					StatusFlag: filesystem.FsOpr_Error,
 				}
 				js, _ := json.Marshal(stopStatus)
 				c.WriteMessage(1, js)
 				c.Close()
+				//Remove the task from ongoing tasks list
+				wsConnectionStore.Delete(oprId)
 				return
 			}
 			rsrc, err := thisSrcFsh.FileSystemAbstraction.VirtualPathToRealPath(subpath, userinfo.Username)
@@ -1425,10 +1464,13 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 					LatestFile: filepath.Base(rsrc),
 					Progress:   -1,
 					Error:      "File not exists",
+					StatusFlag: filesystem.FsOpr_Error,
 				}
 				js, _ := json.Marshal(stopStatus)
 				c.WriteMessage(1, js)
 				c.Close()
+				//Remove the task from ongoing tasks list
+				wsConnectionStore.Delete(oprId)
 				return
 			}
 
@@ -1444,15 +1486,18 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 		}
 
 		//Create the zip file
-		err = filesystem.ArozZipFileWithProgress(sourceFileFsh, realSourceFiles, zipDestFsh, zipDestPath, false, func(currentFilename string, _ int, _ int, progress float64) {
+		err = filesystem.ArozZipFileWithProgress(sourceFileFsh, realSourceFiles, zipDestFsh, zipDestPath, false, func(currentFilename string, _ int, _ int, progress float64) int {
+			sig, _ := UpdateOngoingFileOperation(oprId, currentFilename, math.Ceil(progress))
 			currentStatus := ProgressUpdate{
 				LatestFile: currentFilename,
 				Progress:   int(math.Ceil(progress)),
 				Error:      "",
+				StatusFlag: sig,
 			}
 
 			js, _ := json.Marshal(currentStatus)
 			c.WriteMessage(1, js)
+			return sig
 		})
 
 		if err != nil {
@@ -1479,10 +1524,14 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 				LatestFile: filepath.Base(vdestFile),
 				Progress:   -1,
 				Error:      "Access Denied: No Write Permission",
+				StatusFlag: filesystem.FsOpr_Error,
 			}
 			js, _ := json.Marshal(stopStatus)
 			c.WriteMessage(1, js)
 			c.Close()
+			//Remove the task from ongoing tasks list
+			wsConnectionStore.Delete(oprId)
+			return
 		}
 
 		//Create the destination folder
@@ -1497,10 +1546,14 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 					LatestFile: filepath.Base(vsrcs),
 					Progress:   -1,
 					Error:      "File not exists",
+					StatusFlag: filesystem.FsOpr_Error,
 				}
 				js, _ := json.Marshal(stopStatus)
 				c.WriteMessage(1, js)
 				c.Close()
+				//Remove the task from ongoing tasks list
+				wsConnectionStore.Delete(oprId)
+				return
 			}
 			thisSrcFshAbs := thisSrcFsh.FileSystemAbstraction
 			rsrc, err := thisSrcFshAbs.VirtualPathToRealPath(subpath, userinfo.Username)
@@ -1509,10 +1562,14 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 					LatestFile: filepath.Base(rsrc),
 					Progress:   -1,
 					Error:      "File not exists",
+					StatusFlag: filesystem.FsOpr_Error,
 				}
 				js, _ := json.Marshal(stopStatus)
 				c.WriteMessage(1, js)
 				c.Close()
+				//Remove the task from ongoing tasks list
+				wsConnectionStore.Delete(oprId)
+				return
 			}
 			if thisSrcFsh.RequireBuffer {
 				localBufferFilepath, err := bufferRemoteFileToLocal(thisSrcFsh, rsrc, false)
@@ -1521,10 +1578,14 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 						LatestFile: filepath.Base(rsrc),
 						Progress:   -1,
 						Error:      "Failed to buffer file to local disk",
+						StatusFlag: filesystem.FsOpr_Error,
 					}
 					js, _ := json.Marshal(stopStatus)
 					c.WriteMessage(1, js)
 					c.Close()
+					//Remove the task from ongoing tasks list
+					wsConnectionStore.Delete(oprId)
+					return
 				}
 				realSourceFiles = append(realSourceFiles, localBufferFilepath)
 			} else {
@@ -1539,16 +1600,19 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 		}
 
 		//Unzip the files
-		filesystem.ArozUnzipFileWithProgress(realSourceFiles, unzipDest, func(currentFile string, filecount int, totalfile int, progress float64) {
+		filesystem.ArozUnzipFileWithProgress(realSourceFiles, unzipDest, func(currentFile string, filecount int, totalfile int, progress float64) int {
 			//Generate the status update struct
+			sig, _ := UpdateOngoingFileOperation(oprId, filepath.Base(currentFile), math.Ceil(progress))
 			currentStatus := ProgressUpdate{
 				LatestFile: filepath.Base(currentFile),
 				Progress:   int(math.Ceil(progress)),
 				Error:      "",
+				StatusFlag: sig,
 			}
-
 			js, _ := json.Marshal(currentStatus)
 			c.WriteMessage(1, js)
+
+			return sig
 		})
 
 		if destFsh.RequireBuffer {
@@ -1574,6 +1638,8 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 	} else {
 		//Other operations that allow multiple source files to handle one by one
 		for i := 0; i < len(sourceFiles); i++ {
+			//TODO: REMOVE DEBUG
+			time.Sleep(3 * time.Second)
 			vsrcFile := sourceFiles[i]
 			thisSrcFsh, subpath, err := GetFSHandlerSubpathFromVpath(vsrcFile)
 			if err != nil {
@@ -1581,10 +1647,13 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 					LatestFile: filepath.Base(vsrcFile),
 					Progress:   -1,
 					Error:      "File not exists",
+					StatusFlag: filesystem.FsOpr_Error,
 				}
 				js, _ := json.Marshal(stopStatus)
 				c.WriteMessage(1, js)
 				c.Close()
+				//Remove the task from ongoing tasks list
+				wsConnectionStore.Delete(oprId)
 				return
 			}
 			thisSrcFshAbs := thisSrcFsh.FileSystemAbstraction
@@ -1596,28 +1665,34 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 					LatestFile: filepath.Base(rsrcFile),
 					Progress:   -1,
 					Error:      "File not exists",
+					StatusFlag: filesystem.FsOpr_Error,
 				}
 				js, _ := json.Marshal(stopStatus)
 				c.WriteMessage(1, js)
 				c.Close()
+				//Remove the task from ongoing tasks list
+				wsConnectionStore.Delete(oprId)
 				return
 			}
 
 			if operation == "move" {
-				err := filesystem.FileMove(thisSrcFsh, rsrcFile, destFsh, rdestFile, existsOpr, true, func(progress int, currentFile string) {
+				err := filesystem.FileMove(thisSrcFsh, rsrcFile, destFsh, rdestFile, existsOpr, true, func(progress int, currentFile string) int {
 					//Multply child progress to parent progress
 					blockRatio := float64(100) / float64(len(sourceFiles))
 					overallRatio := blockRatio*float64(i) + blockRatio*(float64(progress)/float64(100))
 
 					//Construct return struct
+					sig, _ := UpdateOngoingFileOperation(oprId, filepath.Base(currentFile), math.Ceil(overallRatio))
 					currentStatus := ProgressUpdate{
 						LatestFile: filepath.Base(currentFile),
 						Progress:   int(overallRatio),
 						Error:      "",
+						StatusFlag: sig,
 					}
 
 					js, _ := json.Marshal(currentStatus)
 					c.WriteMessage(1, js)
+					return sig
 				})
 
 				//Handle move starting error
@@ -1626,10 +1701,13 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 						LatestFile: filepath.Base(rsrcFile),
 						Progress:   -1,
 						Error:      err.Error(),
+						StatusFlag: filesystem.FsOpr_Error,
 					}
 					js, _ := json.Marshal(stopStatus)
 					c.WriteMessage(1, js)
 					c.Close()
+					//Remove the task from ongoing tasks list
+					wsConnectionStore.Delete(oprId)
 					return
 				}
 
@@ -1637,20 +1715,22 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 				metadata.RemoveCache(thisSrcFsh, rsrcFile)
 
 			} else if operation == "copy" {
-				err := filesystem.FileCopy(thisSrcFsh, rsrcFile, destFsh, rdestFile, existsOpr, func(progress int, currentFile string) {
+				err := filesystem.FileCopy(thisSrcFsh, rsrcFile, destFsh, rdestFile, existsOpr, func(progress int, currentFile string) int {
 					//Multply child progress to parent progress
 					blockRatio := float64(100) / float64(len(sourceFiles))
 					overallRatio := blockRatio*float64(i) + blockRatio*(float64(progress)/float64(100))
 
 					//Construct return struct
+					sig, _ := UpdateOngoingFileOperation(oprId, filepath.Base(currentFile), math.Ceil(overallRatio))
 					currentStatus := ProgressUpdate{
 						LatestFile: filepath.Base(currentFile),
 						Progress:   int(overallRatio),
 						Error:      "",
+						StatusFlag: sig,
 					}
-
 					js, _ := json.Marshal(currentStatus)
 					c.WriteMessage(1, js)
+					return sig
 				})
 
 				//Handle Copy starting error
@@ -1659,16 +1739,23 @@ func system_fs_handleWebSocketOpr(w http.ResponseWriter, r *http.Request) {
 						LatestFile: filepath.Base(rsrcFile),
 						Progress:   -1,
 						Error:      err.Error(),
+						StatusFlag: filesystem.FsOpr_Error,
 					}
 					js, _ := json.Marshal(stopStatus)
 					c.WriteMessage(1, js)
 					c.Close()
+					//Remove the task from ongoing tasks list
+					wsConnectionStore.Delete(oprId)
 					return
 				}
 			}
 		}
 	}
 
+	//Remove the task from ongoing tasks list
+	//TODO: REMOVE DEBUG
+	wsConnectionStore.Delete(oprId)
+
 	//Close WebSocket connection after finished
 	time.Sleep(1 * time.Second)
 	c.WriteControl(8, []byte{}, time.Now().Add(time.Second))
@@ -3239,3 +3326,113 @@ func cleanFsBufferFileFromList(filelist []string) {
 		}
 	}
 }
+
+/*
+	File operation load and resume features
+*/
+
+// Handle all the on going task requests.
+// Accept parameter: flag={continue / pause / stop}
+func system_fs_HandleOnGoingTasks(w http.ResponseWriter, r *http.Request) {
+	//Get the user information
+	userinfo, err := userHandler.GetUserInfoFromRequest(w, r)
+	if err != nil {
+		utils.SendErrorResponse(w, "User not logged in")
+		return
+	}
+
+	statusFlag, _ := utils.PostPara(r, "flag")
+	oprid, _ := utils.PostPara(r, "oprid")
+
+	if statusFlag == "" {
+		//No flag defined. Print all operations
+		ongoingTasks := GetAllOngoingFileOperationForUser(userinfo.Username)
+		js, _ := json.Marshal(ongoingTasks)
+		utils.SendJSONResponse(w, string(js))
+	} else if statusFlag != "" {
+		if oprid == "" {
+			utils.SendErrorResponse(w, "oprid is empty or not set")
+			return
+		}
+
+		//Get the operation record
+		oprRecord, err := GetOngoingFileOperationByOprID(oprid)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		if statusFlag == "continue" {
+			//Continue the file operation
+			oprRecord.FileOperationSignal = filesystem.FsOpr_Continue
+		} else if statusFlag == "pause" {
+			//Pause the file operation until the flag is set to other status
+			oprRecord.FileOperationSignal = filesystem.FsOpr_Pause
+		} else if statusFlag == "cancel" {
+			//Cancel and stop the operation
+			oprRecord.FileOperationSignal = filesystem.FsOpr_Cancel
+		} else {
+			utils.SendErrorResponse(w, "unsupported operation")
+			return
+		}
+
+		SetOngoingFileOperation(oprRecord)
+
+		utils.SendOK(w)
+	} else if oprid != "" && statusFlag == "" {
+		//Get the operation record
+		oprRecord, err := GetOngoingFileOperationByOprID(oprid)
+		if err != nil {
+			utils.SendErrorResponse(w, err.Error())
+			return
+		}
+
+		js, _ := json.Marshal(oprRecord)
+		utils.SendJSONResponse(w, string(js))
+
+	}
+
+}
+
+func GetAllOngoingFileOperationForUser(username string) []*fileOperationTask {
+	results := []*fileOperationTask{}
+	wsConnectionStore.Range(func(key, value interface{}) bool {
+		//oprid := key.(string)
+		taskInfo := value.(*fileOperationTask)
+		if taskInfo.Owner == username {
+			results = append(results, taskInfo)
+		}
+		return true
+	})
+
+	return results
+}
+
+// Get an ongoing task record
+func GetOngoingFileOperationByOprID(oprid string) (*fileOperationTask, error) {
+	object, ok := wsConnectionStore.Load(oprid)
+	if !ok {
+		return nil, errors.New("task not exists")
+	}
+
+	return object.(*fileOperationTask), nil
+}
+
+// Set or update an ongoing task record
+func SetOngoingFileOperation(opr *fileOperationTask) {
+	wsConnectionStore.Store(opr.ID, opr)
+}
+
+// Update the status of an onging task record, return latest status code and error if any
+func UpdateOngoingFileOperation(oprid string, currentFile string, progress float64) (int, error) {
+	t, err := GetOngoingFileOperationByOprID(oprid)
+	if err != nil {
+		return 0, err
+	}
+
+	t.LatestFile = currentFile
+	t.Progress = progress
+
+	SetOngoingFileOperation(t)
+	return t.FileOperationSignal, nil
+}

+ 92 - 30
mod/filesystem/fileOpr.go

@@ -32,7 +32,7 @@ import (
 	archiver "github.com/mholt/archiver/v3"
 )
 
-//A basic file zipping function
+// A basic file zipping function
 func ZipFile(filelist []string, outputfile string, includeTopLevelFolder bool) error {
 	z := archiver.Zip{
 		CompressionLevel:       flate.DefaultCompression,
@@ -46,7 +46,7 @@ func ZipFile(filelist []string, outputfile string, includeTopLevelFolder bool) e
 	return err
 }
 
-//A basic file unzip function
+// A basic file unzip function
 func Unzip(source, destination string) error {
 	archive, err := zip.OpenReader(source)
 	if err != nil {
@@ -88,8 +88,8 @@ func Unzip(source, destination string) error {
 	return nil
 }
 
-//Aroz Unzip File with progress update function  (current filename / current file count / total file count / progress in percentage)
-func ArozUnzipFileWithProgress(filelist []string, outputfile string, progressHandler func(string, int, int, float64)) error {
+// Aroz Unzip File with progress update function  (current filename / current file count / total file count / progress in percentage)
+func ArozUnzipFileWithProgress(filelist []string, outputfile string, progressHandler func(string, int, int, float64) int) error {
 	//Gether the total number of files in all zip files
 	totalFileCounts := 0
 	unzippedFileCount := 0
@@ -133,7 +133,16 @@ func ArozUnzipFileWithProgress(filelist []string, outputfile string, progressHan
 				//Folder is created already be the steps above.
 				//Update the progress
 				unzippedFileCount++
-				progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
+				statusCode := progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
+				for statusCode == 1 {
+					//Wait for the task to be resumed
+					time.Sleep(1 * time.Second)
+					statusCode = progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
+				}
+				if statusCode == 2 {
+					//Cancel
+					return errors.New("Operation cancelled by user")
+				}
 				continue
 			}
 
@@ -155,7 +164,16 @@ func ArozUnzipFileWithProgress(filelist []string, outputfile string, progressHan
 
 			//Update the progress
 			unzippedFileCount++
-			progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
+			statusCode := progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
+			for statusCode == 1 {
+				//Wait for the task to be resumed
+				time.Sleep(1 * time.Second)
+				statusCode = progressHandler(file.Name, unzippedFileCount, totalFileCounts, float64(unzippedFileCount)/float64(totalFileCounts)*100.0)
+			}
+			if statusCode == 2 {
+				//Cancel
+				return errors.New("Operation cancelled by user")
+			}
 		}
 	}
 
@@ -163,12 +181,12 @@ func ArozUnzipFileWithProgress(filelist []string, outputfile string, progressHan
 }
 
 /*
-	Aroz Zip File with progress update function
-	Returns the following progress: (current filename / current file count / total file count / progress in percentage)
-	if output is local path that is out of the scope of any fsh, leave outputFsh as nil
+Aroz Zip File with progress update function
+Returns the following progress: (current filename / current file count / total file count / progress in percentage)
+if output is local path that is out of the scope of any fsh, leave outputFsh as nil
 */
-func ArozZipFileWithProgress(targetFshs []*FileSystemHandler, filelist []string, outputFsh *FileSystemHandler, outputfile string, includeTopLevelFolder bool, progressHandler func(string, int, int, float64)) error {
-	fmt.Println("WEBSOCKET ZIPPING", targetFshs, filelist)
+func ArozZipFileWithProgress(targetFshs []*FileSystemHandler, filelist []string, outputFsh *FileSystemHandler, outputfile string, includeTopLevelFolder bool, progressHandler func(string, int, int, float64) int) error {
+	//fmt.Println("WEBSOCKET ZIPPING", targetFshs, filelist)
 	//Get the file count from the filelist
 	totalFileCount := 0
 	for i, srcpath := range filelist {
@@ -250,7 +268,16 @@ func ArozZipFileWithProgress(targetFshs []*FileSystemHandler, filelist []string,
 
 				//Update the zip progress
 				currentFileCount++
-				progressHandler(arozfs.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
+				statusCode := progressHandler(arozfs.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
+				for statusCode == 1 {
+					//Wait for the task to be resumed
+					time.Sleep(1 * time.Second)
+					statusCode = progressHandler(arozfs.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
+				}
+				if statusCode == 2 {
+					//Cancel
+					return errors.New("Operation cancelled by user")
+				}
 				return nil
 			})
 
@@ -282,8 +309,16 @@ func ArozZipFileWithProgress(targetFshs []*FileSystemHandler, filelist []string,
 
 			//Update the zip progress
 			currentFileCount++
-			progressHandler(arozfs.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
-
+			statusCode := progressHandler(arozfs.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
+			for statusCode == 1 {
+				//Wait for the task to be resumed
+				time.Sleep(1 * time.Second)
+				statusCode = progressHandler(arozfs.Base(srcpath), currentFileCount, totalFileCount, (float64(currentFileCount)/float64(totalFileCount))*float64(100))
+			}
+			if statusCode == 2 {
+				//Cancel
+				return errors.New("Operation cancelled by user")
+			}
 		}
 	}
 
@@ -291,10 +326,10 @@ func ArozZipFileWithProgress(targetFshs []*FileSystemHandler, filelist []string,
 }
 
 /*
-	ArozZipFile
-	Zip file without progress update, support local file system or buffer space
-	To use it with local file system, pass in nil in fsh for each item in filelist, e.g.
-	filesystem.ArozZipFile([]*filesystem.FileSystemHandler{nil}, []string{zippingSource}, nil, targetZipFilename, false)
+ArozZipFile
+Zip file without progress update, support local file system or buffer space
+To use it with local file system, pass in nil in fsh for each item in filelist, e.g.
+filesystem.ArozZipFile([]*filesystem.FileSystemHandler{nil}, []string{zippingSource}, nil, targetZipFilename, false)
 */
 func ArozZipFile(sourceFshs []*FileSystemHandler, filelist []string, outputFsh *FileSystemHandler, outputfile string, includeTopLevelFolder bool) error {
 	//Create the target zip file
@@ -485,7 +520,7 @@ func ViewZipFile(filepath string) ([]string, error) {
 	return filelist, err
 }
 
-func FileCopy(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler, dest string, mode string, progressUpdate func(int, string)) error {
+func FileCopy(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler, dest string, mode string, progressUpdate func(int, string) int) error {
 	srcFshAbs := srcFsh.FileSystemAbstraction
 	destFshAbs := destFsh.FileSystemAbstraction
 	if srcFshAbs.IsDir(src) && strings.HasPrefix(dest, src) {
@@ -560,13 +595,22 @@ func FileCopy(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler,
 
 		if progressUpdate != nil {
 			//Set progress to 100, leave it to upper level abstraction to handle
-			progressUpdate(100, arozfs.Base(realDest))
+			statusCode := progressUpdate(100, arozfs.Base(realDest))
+			for statusCode == 1 {
+				//Wait for the task to be resumed
+				time.Sleep(1 * time.Second)
+				statusCode = progressUpdate(100, arozfs.Base(realDest))
+			}
+			if statusCode == 2 {
+				//Cancel
+				return errors.New("Operation cancelled by user")
+			}
 		}
 	}
 	return nil
 }
 
-func FileMove(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler, dest string, mode string, fastMove bool, progressUpdate func(int, string)) error {
+func FileMove(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler, dest string, mode string, fastMove bool, progressUpdate func(int, string) int) error {
 	srcAbst := srcFsh.FileSystemAbstraction
 	destAbst := destFsh.FileSystemAbstraction
 
@@ -660,7 +704,16 @@ func FileMove(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler,
 
 		//Update the progress
 		if progressUpdate != nil {
-			progressUpdate(100, arozfs.Base(src))
+			statusCode := progressUpdate(100, arozfs.Base(src))
+			for statusCode == 1 {
+				//Wait for the task to be resumed
+				time.Sleep(1 * time.Second)
+				statusCode = progressUpdate(100, arozfs.Base(realDest))
+			}
+			if statusCode == 2 {
+				//Cancel
+				return errors.New("Operation cancelled by user")
+			}
 		}
 
 		f, err := srcAbst.ReadStream(src)
@@ -695,13 +748,13 @@ func FileMove(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler,
 	return nil
 }
 
-//Copy a given directory, with no progress udpate
+// Copy a given directory, with no progress udpate
 func CopyDir(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler, dest string) error {
-	return dirCopy(srcFsh, src, destFsh, dest, func(progress int, name string) {})
+	return dirCopy(srcFsh, src, destFsh, dest, func(progress int, name string) int { return 0 })
 }
 
-//Replacment of the legacy dirCopy plugin with filepath.Walk function. Allowing real time progress update to front end
-func dirCopy(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler, realDest string, progressUpdate func(int, string)) error {
+// Replacment of the legacy dirCopy plugin with filepath.Walk function. Allowing real time progress update to front end
+func dirCopy(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler, realDest string, progressUpdate func(int, string) int) error {
 	srcFshAbs := srcFsh.FileSystemAbstraction
 	destFshAbs := destFsh.FileSystemAbstraction
 	//Get the total file counts
@@ -742,13 +795,22 @@ func dirCopy(srcFsh *FileSystemHandler, src string, destFsh *FileSystemHandler,
 
 			//Update move progress
 			if progressUpdate != nil {
-				progressUpdate(int(float64(fileCounter)/float64(totalFileCounts)*100), arozfs.Base(fileSrc))
+				statusCode := progressUpdate(int(float64(fileCounter)/float64(totalFileCounts)*100), arozfs.Base(fileSrc))
+				for statusCode == 1 {
+					//Wait for the task to be resumed
+					time.Sleep(1 * time.Second)
+					statusCode = progressUpdate(int(float64(fileCounter)/float64(totalFileCounts)*100), arozfs.Base(fileSrc))
+				}
+				if statusCode == 2 {
+					//Cancel
+					return errors.New("Operation cancelled by user")
+				}
 			}
 
 			//Move the file using BLFC
 			f, err := srcFshAbs.ReadStream(fileSrc)
 			if err != nil {
-				fmt.Println(err)
+				log.Println(err)
 				return err
 			}
 			defer f.Close()
@@ -838,7 +900,7 @@ func BufferedLargeFileCopy(src string, dst string, BUFFERSIZE int64) error {
 }
 */
 
-//Check if a local path is dir, do not use with file system abstraction realpath
+// Check if a local path is dir, do not use with file system abstraction realpath
 func IsDir(path string) bool {
 	if !FileExists(path) {
 		return false
@@ -857,7 +919,7 @@ func IsDir(path string) bool {
 	return false
 }
 
-//Unzip tar.gz file, use for unpacking web.tar.gz for lazy people
+// Unzip tar.gz file, use for unpacking web.tar.gz for lazy people
 func ExtractTarGzipFile(filename string, outfile string) error {
 	f, err := os.Open(filename)
 	if err != nil {

+ 16 - 8
mod/filesystem/static.go

@@ -23,8 +23,16 @@ import (
 	"imuslab.com/arozos/mod/filesystem/shortcut"
 )
 
-//Structure definations
+// Control Signals for background file operation tasks
+const (
+	FsOpr_Continue  = 0 //Continue file operations
+	FsOpr_Pause     = 1 //Pause and wait until opr back to continue
+	FsOpr_Cancel    = 2 //Cancel and finish the file operation
+	FsOpr_Error     = 3 //Error occured in recent sections
+	FsOpr_Completed = 4 //Operation completed. Delete pending
+)
 
+// Structure definations
 type FileData struct {
 	Filename    string
 	Filepath    string
@@ -83,12 +91,12 @@ var DefaultEmptyHierarchySpecificConfig = EmptyHierarchySpecificConfig{
 	HierarchyType: "placeholder",
 }
 
-//Check if the two file system are identical.
+// Check if the two file system are identical.
 func MatchingFileSystem(fsa *FileSystemHandler, fsb *FileSystemHandler) bool {
 	return fsa.Filesystem == fsb.Filesystem
 }
 
-//Get the ID part of a virtual path, return ID, subpath and error
+// Get the ID part of a virtual path, return ID, subpath and error
 func GetIDFromVirtualPath(vpath string) (string, string, error) {
 	if !strings.Contains(vpath, ":") {
 		return "", "", errors.New("Path missing Virtual Device ID. Given: " + vpath)
@@ -225,8 +233,8 @@ func IsInsideHiddenFolder(path string) bool {
 }
 
 /*
-	Wildcard Replacement Glob, design to hanle path with [ or ] inside.
-	You can also pass in normal path for globing if you are not sure.
+Wildcard Replacement Glob, design to hanle path with [ or ] inside.
+You can also pass in normal path for globing if you are not sure.
 */
 func WGlob(path string) ([]string, error) {
 	files, err := filepath.Glob(path)
@@ -257,8 +265,8 @@ func WGlob(path string) ([]string, error) {
 }
 
 /*
-	Get Directory size, require filepath and include Hidden files option(true / false)
-	Return total file size and file count
+Get Directory size, require filepath and include Hidden files option(true / false)
+Return total file size and file count
 */
 func GetDirctorySize(filename string, includeHidden bool) (int64, int) {
 	var size int64 = 0
@@ -378,7 +386,7 @@ func UnderTheSameRoot(srcAbs string, destAbs string) (bool, error) {
 	return false, nil
 }
 
-//Get the physical root of a given filepath, e.g. C: or /home
+// Get the physical root of a given filepath, e.g. C: or /home
 func GetPhysicalRootFromPath(filename string) (string, error) {
 	filename, err := filepath.Abs(filename)
 	if err != nil {

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

@@ -4727,8 +4727,6 @@
                 }
             });
         }
-
-        let uploadBufferedRefreshTimer;
        
         function uploadFile(file, uuid=undefined, targetDir=undefined) {
             if (file.size > postUploadModeCutoff && lowMemoryMode){

+ 145 - 36
web/SystemAO/file_system/file_operation.html

@@ -13,7 +13,9 @@
                 overflow: hidden;
             }
             .banner{
-                background-color:#4287f5;
+                /*background-color:#2b4871;*/
+                background: rgb(43,72,113);
+                background: linear-gradient(153deg, rgba(43,72,113,1) 43%, rgba(141,146,147,1) 100%); 
                 height:50px;
                 padding:12px;
                 padding-left:20px;
@@ -54,6 +56,10 @@
             <div class="ui active small progress" style="margin-top:18px;">
                 <div id="progressbar" class="bar" style="width:100%; background-color:#4287f5;"></div>
             </div>
+            <div id="advanceControlbtns" class="ui right floated small buttons" style="margin-top: -2em; display: none;">
+                <div id="pauseButton" locale="button/pause" class="ui basic grey button" onclick="togglePauseTask();">Pause</div>
+                <div id="cancelButton" class="ui basic red button" locale="button/cancel" onclick="cancelTask();">Cancel</div>
+            </div>
         </div>
         <div class="ui modal" id="duplicateAction">
             <div class="content">
@@ -97,6 +103,9 @@
             */
             var operationConfig = null;
             var opr = "";
+            var oprId = "";
+            var originalIcon = "";
+            var paused = false;
             var maxPathDisplayLength = 40;
             var legacyMode = !('WebSocket' in window || 'MozWebSocket' in window); //Use AJAX instead of WebSocket if legacy mode is activated
             var enterErrorMode = false;
@@ -216,21 +225,26 @@
                 //Check which type of the oprs is about. And assign the related functions to start
                 if (operationConfig.opr == "move"){
                     $("#opricon").attr("src","img/move.gif");
+                    originalIcon = "img/move.gif";
                     cut(operationConfig.src, operationConfig.dest, operationConfig.overwriteMode);
                 
                 }else if (operationConfig.opr == "copy"){
                     $("#opricon").attr("src","img/copy.gif");
+                    originalIcon = "img/copy.gif"
                     copyopr(operationConfig.src, operationConfig.dest, operationConfig.overwriteMode);
                 
                 }else if (operationConfig.opr == "zip"){
                     $("#opricon").attr("src","img/zip.gif");
+                    originalIcon = "img/zip.gif"
                     zip(operationConfig.src, operationConfig.dest, operationConfig.overwriteMode);
                 
                 }else if (operationConfig.opr == "unzip"){
                     $("#opricon").attr("src","img/unzip.gif");
+                    originalIcon = "img/unzip.gif"
                     unzip(operationConfig.src, operationConfig.dest, operationConfig.overwriteMode);
                 }else if (operationConfig.opr == "unzipAndOpen"){
                     $("#opricon").attr("src","img/unzip.gif");
+                    originalIcon = "img/unzip.gif"
                     unzip(operationConfig.src, operationConfig.dest, operationConfig.overwriteMode, function(){
                         //Open the target directory
 
@@ -273,29 +287,27 @@
                         var data = evt.data;
                         var progress = JSON.parse(data);
                         console.log(progress);
-                        if (progress.Error != ""){
+                        if (progress.oprid != undefined){
+                            //This operation is assigned an ongoing task id. Show pause and cancel
+                            showPauseCancelButton();
+                            oprId = progress.oprid;
+                        }else if (progress.Error != ""){
                             //Something went wrong
                             $("#progressbar").css("background-color", "#eb3f28");
                             enterErrorMode = true;
                             handleFinish({error: progress.Error});
                         }else{
                             //Update the progress display
-                            $("#progressbar").css("width", progress.Progress + "%");
                             var currentSrc = truncate(progress.LatestFile, maxPathDisplayLength);
                             var filteredDest = operationConfig.dest.trim();
                             if (filteredDest.substr(filteredDest.length -1, 1) != "/"){
                                 filteredDest += "/"
                             }
                             var currentDest = truncate(filteredDest + progress.LatestFile, maxPathDisplayLength);
-
                             $("#src").text(currentSrc);
                             $("#dest").text(currentDest);
-                            $("#progress").text(progress.Progress + "%")
 
-                            if (progress.Progress == 100){
-                                //Set progress bar to green
-                                $("#progressbar").css("background-color", "#2bba35");
-                            }
+                            handleProgressUpdate(progress);
                         }
                     };
 
@@ -371,29 +383,27 @@
                     ws.onmessage = function (evt) { 
                         var data = evt.data;
                         var progress = JSON.parse(data);
-                        if (progress.Error != ""){
+                        if (progress.oprid != undefined){
+                            //This operation is assigned an ongoing task id. Show pause and cancel
+                            showPauseCancelButton();
+                            oprId = progress.oprid;
+                        }else if (progress.Error != ""){
                             //Something went wrong
                             $("#progressbar").css("background-color", "#eb3f28");
                             enterErrorMode = true;
                             handleFinish({error: progress.Error});
                         }else{
                             //Update the progress display
-                            $("#progressbar").css("width", progress.Progress + "%");
                             var currentSrc = truncate(srcZipRoot + "/" + progress.LatestFile, maxPathDisplayLength);
                             var filteredDest = operationConfig.dest.trim();
                             if (filteredDest.substr(filteredDest.length -1, 1) != "/"){
                                 filteredDest += "/"
                             }
                             var currentDest = truncate(filteredDest + progress.LatestFile, maxPathDisplayLength);
-
                             $("#src").text(currentSrc);
                             $("#dest").text(currentDest);
-                            $("#progress").text(progress.Progress + "%")
-
-                            if (progress.Progress == 100){
-                                //Set progress bar to green
-                                $("#progressbar").css("background-color", "#2bba35");
-                            }
+                            handleProgressUpdate(progress);
+                           
                         }
                     };
                             
@@ -449,13 +459,16 @@
                     ws.onmessage = function (evt) { 
                         var data = evt.data;
                         var progress = JSON.parse(data);
-                        if (progress.Error != ""){
+                        if (progress.oprid != undefined){
+                            //This operation is assigned an ongoing task id. Show pause and cancel
+                            showPauseCancelButton();
+                            oprId = progress.oprid;
+                        }else if (progress.Error != ""){
                             $("#progressbar").css("background-color", "#eb3f28");
                             enterErrorMode = true;
                             handleFinish({error: progress.Error});
                            
                         }else{
-                            $("#progressbar").css("width", progress.Progress + "%");
                             var currentSrc = truncate(operationConfig.src + "/" + progress.LatestFile, maxPathDisplayLength);
                             var filteredDest = operationConfig.dest.trim();
                             if (filteredDest.substr(filteredDest.length -1, 1) != "/"){
@@ -464,12 +477,8 @@
                             var currentDest = truncate(filteredDest + progress.LatestFile, maxPathDisplayLength);
                             $("#src").text(currentSrc);
                             $("#dest").text(currentDest);
-                            $("#progress").text(progress.Progress + "%")
 
-                            if (progress.Progress == 100){
-                                //Set progress bar to green
-                                $("#progressbar").css("background-color", "#2bba35");
-                            }
+                            handleProgressUpdate(progress);
                         }
                         
                     };
@@ -530,27 +539,25 @@
                     ws.onmessage = function (evt) { 
                         var data = evt.data;
                         var progress = JSON.parse(data);
-                        if (progress.Error != ""){
+                        if (progress.oprid != undefined){
+                            //This operation is assigned an ongoing task id. Show pause and cancel
+                            showPauseCancelButton();
+                            oprId = progress.oprid;
+                        }else if (progress.Error != ""){
                             $("#progressbar").css("background-color", "#eb3f28");
                             enterErrorMode = true;
                             handleFinish({error: progress.Error});
                         }else{
-                            $("#progressbar").css("width", progress.Progress + "%");
                             var currentSrc = truncate(operationConfig.src + "/" + progress.LatestFile, maxPathDisplayLength);
                             var filteredDest = operationConfig.dest.trim();
                             if (filteredDest.substr(filteredDest.length -1, 1) != "/"){
                                 filteredDest += "/"
                             }
                             var currentDest = truncate(filteredDest + progress.LatestFile, maxPathDisplayLength);
-
                             $("#src").text(currentSrc);
                             $("#dest").text(currentDest);
-                            $("#progress").text(progress.Progress + "%")
-
-                            if (progress.Progress == 100){
-                                //Set progress bar to green
-                                $("#progressbar").css("background-color", "#2bba35");
-                            }
+                            
+                            handleProgressUpdate(progress);
                         }
                         
                     };
@@ -635,14 +642,43 @@
 
                 $(".title").text(title);
             }
+            
+            function handleProgressUpdate(progress){
+                if (progress.StatusFlag != undefined){
+                    switch (progress.StatusFlag) {
+                        case 1:
+                            if ($("#opricon").attr("src") != "img/paused.png"){
+                                $("#opricon").attr("src", "img/paused.png");
+                            }
+                            break;
+                        case 2:
+                        if ($("#opricon").attr("src") != "img/error.png"){
+                                $("#opricon").attr("src", "img/error.png");
+                            }
+                            break;
+                        default:
+                            if ($("#opricon").attr("src") != originalIcon){
+                                $("#opricon").attr("src",originalIcon);
+                            }
+                    }
+                }
+                $("#progressbar").css("width", progress.Progress + "%");
+                $("#progress").text(progress.Progress + "%")
+                if (progress.Progress == 100){
+                    //Set progress bar to green
+                    $("#progressbar").css("background-color", "#2bba35");
+                }
+            }
 
             function handleFinish(data){
+                $("#advanceControlbtns").find(".button").addClass("disabled");
                 if (data.error !== undefined){
                     $("#progressbar").css("background-color","#db2828");
                     $("#progressbar").parent().removeClass('active');
-                    $(".title").html(`<i class="remove icon"></i>` + applocale.getString("error/" + data.error,data.error));
+                    $(".title").html(applocale.getString("error/" + data.error,data.error));
                     $("#opricon").attr("src", "img/error.png");
                     enterErrorMode = true;
+                    $("#cancelButton").html(applocale.getString("button/cancel", "Cancel"));
                     return
                 }else{
                     $("#progressbar").css("background-color","#21ba45");
@@ -725,7 +761,80 @@
                     }
                 })
             }
-          
+
+            /*
+                Functions related to pause and cancel of the file operations
+            */
+            
+            function togglePauseTask(){
+                if (paused){
+                    //Resume
+                    $.ajax({
+                        url: "../../system/file_system/ongoing",
+                        method: "POST",
+                        data: {"oprid": oprId, "flag": "continue"},
+                        success: function(data){
+                            if (data.error != undefined){
+                                console.log(data.error);
+                            }else{
+                                paused = false;
+                                $("#pauseButton").text(applocale.getString("button/pause","Pause"));
+                                $("#opricon").attr("src",originalIcon);
+                            }
+                        }
+                    });
+                }else{
+                    //Pause
+                    $.ajax({
+                        url: "../../system/file_system/ongoing",
+                        method: "POST",
+                        data: {"oprid": oprId, "flag": "pause"},
+                        success: function(data){
+                            if (data.error != undefined){
+                                console.log(data.error);
+                            }else{
+                                paused = true;
+                                $("#opricon").attr("src","img/paused.png");
+                                $("#pauseButton").text(applocale.getString("button/resume","Resume"));
+                            }
+                        }
+                    });
+                }
+                
+            }
+
+            //Cancel the current ongoing task
+            function cancelTask(){
+                $.ajax({
+                        url: "../../system/file_system/ongoing",
+                        method: "POST",
+                        data: {"oprid": oprId, "flag": "cancel"},
+                        success: function(data){
+                            if (data.error != undefined){
+                                console.log(data.error);
+                            }else{
+                                //Disable further operation untill task cancelled
+                                $("#advanceControlbtns").find(".button").addClass("disabled");
+                                $("#cancelButton").html(`<i class="ui loading spinner icon"></i>`);
+                            }
+                        }
+                    });
+            }
+
+            function showPauseCancelButton(){
+                ao_module_setWindowSize(400,244);
+                $("#progressbar").parent().css({
+                    "margin-bottom":"1em"
+                });
+                $("#advanceControlbtns").show();
+            }
+
+
+            function hidePauseCancelButton(){
+                ao_module_setWindowSize(400,220);
+                $("#progressbar").parent().css("margin-bottom", "2.5em");
+                $("#advanceControlbtns").hide();
+            }
         </script>
     </body>
 </html>

BIN
web/SystemAO/file_system/img/paused.png


+ 6 - 0
web/SystemAO/locale/file_operation.json

@@ -23,6 +23,9 @@
                 "title/unzipping":"正在解壓 ",
                 "title/file":" 個檔案",
                 "title/files":" 個檔案",
+                "button/pause":"暫停",
+                "button/resume":"繼續",
+                "button/cancel":"取消",
                 "error/Access Denied":"存取被拒",
                 "error/Source file not exists":"來源檔案不存在",
                 "error/Source and destination paths are identical.":"目的地與來源資料位置相同",
@@ -32,6 +35,7 @@
                 "error/This source file is Read Only":"此來源檔案是唯讀檔案",
                 "error/File already exists":"檔案已經存在",
                 "error/This directory is Read Only":"此資料夾是唯讀檔案",
+                "error/Operation cancelled by user": "使用者已取消此檔案操作",
                 "":""
             },
             "titles":{
@@ -71,6 +75,7 @@
                 "error/This source file is Read Only":"此來源檔案是唯讀檔案",
                 "error/File already exists":"檔案已經存在",
                 "error/This directory is Read Only":"此資料夾是唯讀檔案",
+                "error/Operation cancelled by user": "使用者已取消此檔案操作",
                 "":""
             },
             "titles":{
@@ -110,6 +115,7 @@
                 "error/This source file is Read Only":"此来源文件是唯读档案",
                 "error/File already exists":"文件已经存在",
                 "error/This directory is Read Only":"此资料夹是唯读档案", 
+                "error/Operation cancelled by user": "使用者已取消此档案操作",
                 "":""
             },
             "titles":{

+ 152 - 17
web/desktop.system

@@ -471,9 +471,9 @@
             background-color: #242330;
         }
         
-        #contextmenu {
+        .contextmenu {
             position: fixed;
-            z-index: 3000;
+            z-index: 3000 !important;
             left: 20px;
             top: 20px;
         }
@@ -566,19 +566,39 @@
         }
 
         .quicktools{
-            padding-left: 0.7em;
+            margin-top: -0.1em;
+            position: relative;
             text-align:center; 
             height: 100%;
         }
 
-        .quicktools:hover{
-            background-color: rgba(240,240,240,0.2);
-        }
-
         .qtwrapper{
-            padding:4px;
-            padding-right: 8px;
             cursor:pointer;
+            display: inline-block;
+            border-radius: 0.4em;
+        }
+
+        .qtwrapper.backgroundtask{
+            padding-left:8px;
+            padding-right: 6px;
+            padding-top: 7px;
+            padding-bottom: 5px;
+        }
+
+        .qtwrapper.backgroundtask i{
+            margin-top: -1px;
+            margin-right: 1px;
+        }
+
+        .qtwrapper.content{
+            padding-left:6px;
+            padding-right: 4px;
+            padding-top: 4px;
+            padding-bottom: 8px;
+        }
+
+        .qtwrapper:hover{
+            background-color: rgba(240,240,240,0.2);
         }
 
         .notificationbar{
@@ -711,6 +731,10 @@
             border: 1px solid #ebebeb;
             padding-top:8px;
             padding-bottom:8px;
+
+            box-shadow: 6px 8px 13px 2px rgba(191,191,191,0.13);
+            -webkit-box-shadow: 6px 8px 13px 2px rgba(191,191,191,0.13);
+            -moz-box-shadow: 6px 8px 13px 2px rgba(191,191,191,0.13);
         }
 
         #quickAccessPanel .item{
@@ -886,18 +910,66 @@
                 padding:2px;
                 word-wrap: break-word;
             }
-    
 
-            
+            /*
+                Background Task UI
+            */
+            #backgroundTaskPanel{
+                background-color:white;
+                position:absolute;
+                top:28px;
+                right:50px;
+                z-index:115;
+                width:400px;
+                border-radius: 10px;
+                border-top-right-radius: 0px;
+                border-top-left-radius: 0px;
+                border: 1px solid #ebebeb;
+                padding-top:8px;
+                padding-bottom:8px;
+
+                box-shadow: 6px 8px 13px 2px rgba(191,191,191,0.13);
+                -webkit-box-shadow: 6px 8px 13px 2px rgba(191,191,191,0.13);
+                -moz-box-shadow: 6px 8px 13px 2px rgba(191,191,191,0.13);
+            }
+
+            .backgroundtaskObject{
+                padding: 0.4em;
+                padding-left: 1.2em;
+            }
+
+            .backgroundtaskObject .progress, .backgroundtaskObject .bar{
+                height: 1em !important;
+            }
+
+            .backgroundtaskObject .bar{
+                min-width: 0px !important;
+            }
+
+            .backgroundtaskObject .progress .bar{
+                background-color: #2b4871;
+            }
 
+            .backgroundtaskObject.paused .progress .bar{
+                background-color: #d6ba3e;
+            }
+
+            .backgroundtaskObject .circular.button i{
+                color: rgb(160, 160, 160) !important;
+                display: inline-block;
+                width: 10px !important;
+            }
 
 
         /*
                 Z-index layering
                 Layer -1 : Background Images
+                Layer 0: Status Bar
                 Layer 0 - 99: Not focused tab
                 Layer 100: Focused tab drag drop layer
                 Layer 101: Focused Tab / File Descriptor Tag
+                Layer 114: Quick Access Panel
+                Layer 115: Background Task Panel
                 ...
                 Layer 400 - 499: Non-focused top most FloatWindows
                 Layer 500: Top Mot FloatWindow drag drop layer
@@ -935,8 +1007,11 @@
         <div class="clock statusbarpadding" onclick="toggleNotification();" ontouchstart="toggleNotification();">
             ArozOS Desktop
         </div>
-        <div class="quicktools statusbarpadding" onclick="showToolPanel();" ontouchend="showToolPanel();">
-            <div class="qtwrapper">
+        <div class="quicktools statusbarpadding">
+            <div class="qtwrapper backgroundtask">
+                <i class="loading circle notch icon"></i>
+            </div>
+            <div class="qtwrapper content" onclick="showToolPanel();" ontouchend="showToolPanel();" style="margin-right: 0.4em;">
                 <i class="content icon"></i>
             </div>
         </div>
@@ -1046,6 +1121,32 @@
         </div>
     </div>
 
+    <!-- Background Task Panel -->
+    <div id="backgroundTaskPanel" class="" style="display:;">
+        <div class="backgroundtaskObject">
+            <small>src > dest (test.mp4)</small>
+            <div class="ui grid">
+                <div class="twelve wide column" style="padding-right: 0.4em;">
+                    <div class="ui mini active progress">
+                        <div class="bar" style="width: 50%;">
+                            <div class="progress">50%</div>
+                        </div>
+                    </div>
+                </div>
+                <div class="four wide column"  style="padding-top: 0.4em; padding-left: 0;" align="right">
+                    <button class="circular ui mini basic icon button">
+                        <i class="pause icon"></i>
+                    </button>
+                    <button class="circular ui mini basic red icon button">
+                        <i class="remove icon" style="color: red !important;"></i>
+                    </button>
+                </div>
+              </div>
+            
+        </div>
+    </div>
+
+    <!-- Content / Quick Access Panel-->
     <div id="quickAccessPanel" class="" style="display:none;">
         <div class="ui small items" style="margin-bottom: 0;">
             <div class="item">
@@ -1164,6 +1265,8 @@
 
         //Upload related
         let uploadFileChunkSize = 1024 * 512; //512KB per chunk in low memory upload
+        let largeFileCutoffSize = 8192 * 1024 * 1024; //Any file larger than this size is consider "large file", default to 8GB
+        let postUploadModeCutoff = 25 * 1048576; //25MB, files smaller than this will upload using POST Mode
 
         //Others
         var monthNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
@@ -1213,6 +1316,7 @@
             hookLaunchMenuEvents();
             initTheme();
             initStartupSounds();
+            initUploadCuttoffValues();
 
             //Login cookie expire check
             setInterval(function() {
@@ -1220,6 +1324,31 @@
             }, 15000);
         }
 
+        function initUploadCuttoffValues(){
+             //Get the avaible space on tmp disk and decide the cutoff file size that need to directly write to disk
+            $.ajax({
+                url: "system/disk/space/tmp",
+                method: "POST",
+                data: {},
+                success: function(data){
+                    if (data.error !== undefined){
+                        console.log("%c[Desktop] Unable to auto-detect huge file cutoff size: " + data.error, 'color: #e64747');
+                    }else{
+                        if (!isNaN(data.Available) && data.Available > 0){
+                            largeFileCutoffSize = data.Available/16 - 4096;
+                            console.log("%c[Desktop] Setting huge file cutoff size at: " + ao_module_utils.formatBytes(data.Available/16), 'color: #036ffc');
+                        }else if (isNaN(data.Available)){
+                            console.log("%c[Desktop] Unable to read available tmp disk size. Using default huge file cutoff size.", 'color: #e64747');
+                        }
+                        
+                    }
+                },
+                error: function(){
+                    //Hardware mode disabled. Use default value.
+                }
+            });
+        }
+
         function handleManualCheckReconnect(button){
             $(button).addClass("loading");
             checkConnection(undefined, function(status){
@@ -6242,7 +6371,7 @@
                         //Check if ramsize > 3.8 GB (4GB). If yes, switch to large memory upload mode
                         var memsize = JSON.parse(data);
                         if (parseFloat(memsize)/ 1024 / 1024 / 1024 >= 3.8){
-                            console.log("%c[Desktop] Setting upload mode to large memory mode", 'color: #2ba16e');
+                            console.log("%c[Desktop] Setting upload mode to large memory mode", 'color: #036ffc');
                             lowMemoryMode = false;
                         }
                     }
@@ -6289,7 +6418,7 @@
         }
 
         function uploadFile(file, callback=undefined, uploadingIconUUID = undefined) {
-            if (lowMemoryMode){
+            if (file.size > postUploadModeCutoff && lowMemoryMode){
                 /*
                     Low Memory Upload Mode
                 */
@@ -6323,7 +6452,14 @@
                     uploadDir = uploadDir + subpath;
                 }
 
-                let socket = new WebSocket(protocol + window.location.hostname + ":" + port + "/system/file_system/lowmemUpload?filename=" + filename + "&path=" + uploadDir);
+                let hugeFileMode = "";
+                if (file.size > largeFileCutoffSize){
+                    //Filesize over cutoff line. Use huge file mode
+                    hugeFileMode = "&hugefile=true";
+                }
+                
+
+                let socket = new WebSocket(protocol + window.location.hostname + ":" + port + "/system/file_system/lowmemUpload?filename=" + filename + "&path=" + uploadDir + hugeFileMode);
                 let currentSendingIndex = 0;
                 let chunks = Math.ceil(file.size/uploadFileChunkSize,uploadFileChunkSize);
                 
@@ -6599,7 +6735,6 @@
                 data: {preference: "themecolor"},
                 method: "POST",
                 success: function(data){
-                    console.log(data);
                     if (data.error == undefined && data != ""){
                         setThemeColor(data);
                     }