mirror of
https://github.com/tobychui/zoraxy.git
synced 2025-08-05 20:58:28 +02:00
489 lines
13 KiB
Go
489 lines
13 KiB
Go
package filemanager
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"imuslab.com/zoraxy/mod/utils"
|
|
)
|
|
|
|
/*
|
|
File Manager
|
|
|
|
This is a simple package that handles file management
|
|
under the web server directory
|
|
*/
|
|
|
|
type FileManager struct {
|
|
Directory string
|
|
}
|
|
|
|
// Create a new file manager with directory as root
|
|
func NewFileManager(directory string) *FileManager {
|
|
return &FileManager{
|
|
Directory: directory,
|
|
}
|
|
}
|
|
|
|
// Handle listing of a given directory
|
|
func (fm *FileManager) HandleList(w http.ResponseWriter, r *http.Request) {
|
|
directory, err := utils.GetPara(r, "dir")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid directory given")
|
|
return
|
|
}
|
|
|
|
// Construct the absolute path to the target directory
|
|
targetDir := filepath.Join(fm.Directory, directory)
|
|
|
|
// Clean path to prevent path escape #274
|
|
isValidRequest := validatePathEscape(targetDir, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Open the target directory
|
|
dirEntries, err := os.ReadDir(targetDir)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to open directory")
|
|
return
|
|
}
|
|
|
|
// Create a slice to hold the file information
|
|
var files []map[string]interface{} = []map[string]interface{}{}
|
|
|
|
// Iterate through the directory entries
|
|
for _, dirEntry := range dirEntries {
|
|
fileInfo := make(map[string]interface{})
|
|
fileInfo["filename"] = dirEntry.Name()
|
|
fileInfo["filepath"] = filepath.Join(directory, dirEntry.Name())
|
|
fileInfo["isDir"] = dirEntry.IsDir()
|
|
|
|
// Get file size and last modified time
|
|
finfo, err := dirEntry.Info()
|
|
if err != nil {
|
|
//unable to load its info. Skip this file
|
|
continue
|
|
}
|
|
fileInfo["lastModified"] = finfo.ModTime().Unix()
|
|
if !dirEntry.IsDir() {
|
|
// If it's a file, get its size
|
|
fileInfo["size"] = finfo.Size()
|
|
} else {
|
|
// If it's a directory, set size to 0
|
|
fileInfo["size"] = 0
|
|
}
|
|
|
|
// Append file info to the list
|
|
files = append(files, fileInfo)
|
|
}
|
|
|
|
// Serialize the file info slice to JSON
|
|
jsonData, err := json.Marshal(files)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to marshal JSON")
|
|
return
|
|
}
|
|
|
|
// Set response headers and send the JSON response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(jsonData)
|
|
}
|
|
|
|
// Handle upload of a file (multi-part), 25MB max
|
|
func (fm *FileManager) HandleUpload(w http.ResponseWriter, r *http.Request) {
|
|
dir, err := utils.PostPara(r, "dir")
|
|
if err != nil {
|
|
log.Println("no dir given")
|
|
utils.SendErrorResponse(w, "invalid dir given")
|
|
return
|
|
}
|
|
|
|
// Parse the multi-part form data
|
|
err = r.ParseMultipartForm(25 << 20)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to parse form data")
|
|
return
|
|
}
|
|
|
|
// Get the uploaded file
|
|
file, fheader, err := r.FormFile("file")
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
utils.SendErrorResponse(w, "unable to get uploaded file")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
// Specify the directory where you want to save the uploaded file
|
|
uploadDir := filepath.Join(fm.Directory, dir)
|
|
|
|
// Clean path to prevent path escape #274
|
|
isValidRequest := validatePathEscape(uploadDir, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
if !utils.FileExists(uploadDir) {
|
|
utils.SendErrorResponse(w, "upload target directory not exists")
|
|
return
|
|
}
|
|
|
|
filename := sanitizeFilename(fheader.Filename)
|
|
if !isValidFilename(filename) {
|
|
utils.SendErrorResponse(w, "filename contain invalid or reserved characters")
|
|
return
|
|
}
|
|
|
|
// Create the file on the server
|
|
filePath := filepath.Join(uploadDir, filepath.Base(filename))
|
|
out, err := os.Create(filePath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to create file on the server")
|
|
return
|
|
}
|
|
defer out.Close()
|
|
|
|
// Copy the uploaded file to the server
|
|
_, err = io.Copy(out, file)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to copy file to server")
|
|
return
|
|
}
|
|
|
|
// Respond with a success message or appropriate response
|
|
utils.SendOK(w)
|
|
}
|
|
|
|
// Handle download of a selected file, serve with content dispose header
|
|
func (fm *FileManager) HandleDownload(w http.ResponseWriter, r *http.Request) {
|
|
filename, err := utils.GetPara(r, "file")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid filepath given")
|
|
return
|
|
}
|
|
|
|
filePath := filepath.Join(fm.Directory, filename)
|
|
// Clean path to prevent path escape #274
|
|
isValidRequest := validatePathEscape(filePath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
previewMode, _ := utils.GetPara(r, "preview")
|
|
if previewMode == "true" {
|
|
// Serve the file using http.ServeFile
|
|
http.ServeFile(w, r, filePath)
|
|
} else {
|
|
// Trigger a download with content disposition headers
|
|
w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(filename))
|
|
http.ServeFile(w, r, filePath)
|
|
}
|
|
}
|
|
|
|
// HandleNewFolder creates a new folder in the specified directory
|
|
func (fm *FileManager) HandleNewFolder(w http.ResponseWriter, r *http.Request) {
|
|
// Parse the directory name from the request
|
|
dirName, err := utils.PostPara(r, "path")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid directory name")
|
|
return
|
|
}
|
|
|
|
//Prevent path escape
|
|
dirName = strings.ReplaceAll(dirName, "\\", "/")
|
|
dirName = strings.ReplaceAll(dirName, "../", "")
|
|
|
|
// Specify the directory where you want to create the new folder
|
|
newFolderPath := filepath.Join(fm.Directory, dirName)
|
|
isValidRequest := validatePathEscape(newFolderPath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Check if the folder already exists
|
|
if _, err := os.Stat(newFolderPath); os.IsNotExist(err) {
|
|
// Create the new folder
|
|
err := os.Mkdir(newFolderPath, os.ModePerm)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to create the new folder")
|
|
return
|
|
}
|
|
|
|
// Respond with a success message or appropriate response
|
|
utils.SendOK(w)
|
|
} else {
|
|
// If the folder already exists, respond with an error
|
|
utils.SendErrorResponse(w, "folder already exists")
|
|
}
|
|
}
|
|
|
|
// HandleFileCopy copies a file or directory from the source path to the destination path
|
|
func (fm *FileManager) HandleFileCopy(w http.ResponseWriter, r *http.Request) {
|
|
// Parse the source and destination paths from the request
|
|
srcPath, err := utils.PostPara(r, "srcpath")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid source path")
|
|
return
|
|
}
|
|
|
|
destPath, err := utils.PostPara(r, "destpath")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid destination path")
|
|
return
|
|
}
|
|
|
|
// Validate and sanitize the source and destination paths
|
|
srcPath = filepath.Clean(srcPath)
|
|
destPath = filepath.Clean(destPath)
|
|
|
|
// Construct the absolute paths
|
|
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
|
absDestPath := filepath.Join(fm.Directory, destPath)
|
|
|
|
//Make sure the copy source and dest are within web directory folder
|
|
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Check if the source path exists
|
|
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
|
utils.SendErrorResponse(w, "source path does not exist")
|
|
return
|
|
}
|
|
|
|
// Check if the destination path exists
|
|
if _, err := os.Stat(absDestPath); os.IsNotExist(err) {
|
|
utils.SendErrorResponse(w, "destination path does not exist")
|
|
return
|
|
}
|
|
|
|
//Join the name to create final paste filename
|
|
absDestPath = filepath.Join(absDestPath, filepath.Base(absSrcPath))
|
|
//Reject opr if already exists
|
|
if utils.FileExists(absDestPath) {
|
|
utils.SendErrorResponse(w, "target already exists")
|
|
return
|
|
}
|
|
|
|
// Perform the copy operation based on whether the source is a file or directory
|
|
if isDir(absSrcPath) {
|
|
// Recursive copy for directories
|
|
err := copyDirectory(absSrcPath, absDestPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, fmt.Sprintf("error copying directory: %v", err))
|
|
return
|
|
}
|
|
} else {
|
|
// Copy a single file
|
|
err := copyFile(absSrcPath, absDestPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, fmt.Sprintf("error copying file: %v", err))
|
|
return
|
|
}
|
|
}
|
|
|
|
utils.SendOK(w)
|
|
}
|
|
|
|
func (fm *FileManager) HandleFileMove(w http.ResponseWriter, r *http.Request) {
|
|
// Parse the source and destination paths from the request
|
|
srcPath, err := utils.PostPara(r, "srcpath")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid source path")
|
|
return
|
|
}
|
|
|
|
destPath, err := utils.PostPara(r, "destpath")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid destination path")
|
|
return
|
|
}
|
|
|
|
// Validate and sanitize the source and destination paths
|
|
srcPath = filepath.Clean(srcPath)
|
|
destPath = filepath.Clean(destPath)
|
|
|
|
// Construct the absolute paths
|
|
absSrcPath := filepath.Join(fm.Directory, srcPath)
|
|
absDestPath := filepath.Join(fm.Directory, destPath)
|
|
|
|
//Make sure move source and target are within web server directory
|
|
isValidRequest := validatePathEscape(absSrcPath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
isValidRequest = validatePathEscape(absDestPath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Check if the source path exists
|
|
if _, err := os.Stat(absSrcPath); os.IsNotExist(err) {
|
|
utils.SendErrorResponse(w, "source path does not exist")
|
|
return
|
|
}
|
|
|
|
// Check if the destination path exists
|
|
if _, err := os.Stat(absDestPath); !os.IsNotExist(err) {
|
|
utils.SendErrorResponse(w, "destination path already exists")
|
|
return
|
|
}
|
|
|
|
// Rename the source to the destination
|
|
err = os.Rename(absSrcPath, absDestPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, fmt.Sprintf("error moving file/directory: %v", err))
|
|
return
|
|
}
|
|
utils.SendOK(w)
|
|
}
|
|
|
|
func (fm *FileManager) HandleFileProperties(w http.ResponseWriter, r *http.Request) {
|
|
// Parse the target file or directory path from the request
|
|
filePath, err := utils.GetPara(r, "file")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid file path")
|
|
return
|
|
}
|
|
|
|
// Construct the absolute path to the target file or directory
|
|
absPath := filepath.Join(fm.Directory, filePath)
|
|
isValidRequest := validatePathEscape(absPath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Check if the target path exists
|
|
_, err = os.Stat(absPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "file or directory does not exist")
|
|
return
|
|
}
|
|
|
|
// Initialize a map to hold file properties
|
|
fileProps := make(map[string]interface{})
|
|
fileProps["filename"] = filepath.Base(absPath)
|
|
fileProps["filepath"] = filePath
|
|
fileProps["isDir"] = isDir(absPath)
|
|
|
|
// Get file size and last modified time
|
|
finfo, err := os.Stat(absPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to retrieve file properties")
|
|
return
|
|
}
|
|
fileProps["lastModified"] = finfo.ModTime().Unix()
|
|
if !isDir(absPath) {
|
|
// If it's a file, get its size
|
|
fileProps["size"] = finfo.Size()
|
|
} else {
|
|
// If it's a directory, calculate its total size containing all child files and folders
|
|
totalSize, err := calculateDirectorySize(absPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to calculate directory size")
|
|
return
|
|
}
|
|
fileProps["size"] = totalSize
|
|
}
|
|
|
|
// Count the number of sub-files and sub-folders
|
|
numSubFiles, numSubFolders, err := countSubFilesAndFolders(absPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to count sub-files and sub-folders")
|
|
return
|
|
}
|
|
fileProps["fileCounts"] = numSubFiles
|
|
fileProps["folderCounts"] = numSubFolders
|
|
|
|
// Serialize the file properties to JSON
|
|
jsonData, err := json.Marshal(fileProps)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "unable to marshal JSON")
|
|
return
|
|
}
|
|
|
|
// Set response headers and send the JSON response
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(jsonData)
|
|
}
|
|
|
|
// HandleFileDelete deletes a file or directory
|
|
func (fm *FileManager) HandleFileDelete(w http.ResponseWriter, r *http.Request) {
|
|
// Parse the target file or directory path from the request
|
|
filePath, err := utils.PostPara(r, "target")
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "invalid file path")
|
|
return
|
|
}
|
|
|
|
// Construct the absolute path to the target file or directory
|
|
absPath := filepath.Join(fm.Directory, filePath)
|
|
isValidRequest := validatePathEscape(absPath, fm.Directory)
|
|
if !isValidRequest {
|
|
http.Error(w, "403 - Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// Check if the target path exists
|
|
_, err = os.Stat(absPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "file or directory does not exist")
|
|
return
|
|
}
|
|
|
|
// Delete the file or directory
|
|
err = os.RemoveAll(absPath)
|
|
if err != nil {
|
|
utils.SendErrorResponse(w, "error deleting file or directory")
|
|
return
|
|
}
|
|
|
|
// Respond with a success message or appropriate response
|
|
utils.SendOK(w)
|
|
}
|
|
|
|
// Return true if the path is within the root path
|
|
func validatePathEscape(reqestPath string, rootPath string) bool {
|
|
reqestPath = filepath.ToSlash(filepath.Clean(reqestPath))
|
|
rootPath = filepath.ToSlash(filepath.Clean(rootPath))
|
|
|
|
requestPathAbs, err := filepath.Abs(reqestPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
rootPathAbs, err := filepath.Abs(rootPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if strings.HasPrefix(requestPathAbs, rootPathAbs) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|