- Implement chunked file upload with 1MB chunk size - Add progress bar with percentage and chunk counter (e.g., 2/5) - Support resuming interrupted uploads - Improve UI with better progress visualization - Add dropzone.js integration for drag-and-drop uploads - Fix progress bar jumping issue in resumable uploads - Add file type icons and size display - Enhance error handling and user feedback
810 lines
18 KiB
Go
810 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type FileInfo struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
IsDir bool `json:"isDir"`
|
|
Size int64 `json:"size"`
|
|
ModTime time.Time `json:"modTime"`
|
|
CanPreview bool `json:"canPreview"`
|
|
CanDownload bool `json:"canDownload"`
|
|
}
|
|
|
|
type UploadResponse struct {
|
|
Success bool `json:"success"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type MoveRequest struct {
|
|
SrcPath string `json:"srcPath"`
|
|
DestPath string `json:"destPath"`
|
|
}
|
|
|
|
type RenameRequest struct {
|
|
OldPath string `json:"oldPath"`
|
|
NewName string `json:"newName"`
|
|
}
|
|
|
|
type CreateDirRequest struct {
|
|
Path string `json:"path"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
type ErrorResponse struct {
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
type WatchEvent struct {
|
|
Type string `json:"type"`
|
|
Path string `json:"path"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
var (
|
|
rootDir string
|
|
watcher *fsnotify.Watcher
|
|
watchChan chan WatchEvent
|
|
)
|
|
|
|
func init() {
|
|
watchChan = make(chan WatchEvent, 100)
|
|
}
|
|
|
|
func getFileType(filename string) string {
|
|
ext := strings.ToLower(filepath.Ext(filename))
|
|
switch ext {
|
|
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
|
|
return "image"
|
|
case ".txt", ".md", ".json", ".xml", ".html", ".css", ".js", ".go", ".py", ".java", ".c", ".cpp", ".h", ".sh":
|
|
return "text"
|
|
case ".pdf":
|
|
return "pdf"
|
|
default:
|
|
return "other"
|
|
}
|
|
}
|
|
|
|
func formatFileSize(size int64) string {
|
|
const unit = 1024
|
|
if size < unit {
|
|
return fmt.Sprintf("%d B", size)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := size / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
func listFiles(c *gin.Context) {
|
|
path := c.Query("path")
|
|
if path == "" {
|
|
path = "."
|
|
}
|
|
|
|
fullPath := filepath.Join(rootDir, path)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"})
|
|
return
|
|
}
|
|
|
|
file, err := os.Open(fullPath)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
entries, err := file.Readdir(-1)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
var files []FileInfo
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if name == "." || name == ".." || name == "static" {
|
|
continue
|
|
}
|
|
|
|
entryPath := filepath.Join(path, name)
|
|
|
|
fileType := getFileType(name)
|
|
canPreview := false
|
|
if !entry.IsDir() {
|
|
canPreview = fileType == "image" || fileType == "text" || fileType == "pdf"
|
|
}
|
|
|
|
files = append(files, FileInfo{
|
|
Name: name,
|
|
Path: entryPath,
|
|
IsDir: entry.IsDir(),
|
|
Size: entry.Size(),
|
|
ModTime: entry.ModTime(),
|
|
CanPreview: canPreview,
|
|
CanDownload: true,
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"files": files,
|
|
"path": path,
|
|
})
|
|
}
|
|
|
|
func previewFile(c *gin.Context) {
|
|
path := c.Query("path")
|
|
if path == "" {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "缺少path参数"})
|
|
return
|
|
}
|
|
|
|
fullPath := filepath.Join(rootDir, path)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"})
|
|
return
|
|
}
|
|
|
|
file, err := os.Open(fullPath)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
fileType := getFileType(info.Name())
|
|
|
|
if fileType == "image" {
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
contentType := http.DetectContentType(data)
|
|
c.Data(http.StatusOK, contentType, data)
|
|
} else if fileType == "text" {
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
c.Data(http.StatusOK, "text/plain; charset=utf-8", data)
|
|
} else if fileType == "pdf" {
|
|
data, err := io.ReadAll(file)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
c.Data(http.StatusOK, "application/pdf", data)
|
|
} else {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "不支持预览此类型文件"})
|
|
}
|
|
}
|
|
|
|
func downloadFile(c *gin.Context) {
|
|
pathsParam := c.Query("paths")
|
|
if pathsParam == "" {
|
|
path := c.Query("path")
|
|
if path == "" {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "缺少path参数"})
|
|
return
|
|
}
|
|
downloadSingle(c, path)
|
|
return
|
|
}
|
|
|
|
var paths []string
|
|
if err := json.Unmarshal([]byte(pathsParam), &paths); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的paths参数"})
|
|
return
|
|
}
|
|
|
|
if len(paths) == 1 {
|
|
downloadSingle(c, paths[0])
|
|
return
|
|
}
|
|
|
|
downloadMultiple(c, paths)
|
|
}
|
|
|
|
func downloadSingle(c *gin.Context, path string) {
|
|
fullPath := filepath.Join(rootDir, path)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"})
|
|
return
|
|
}
|
|
|
|
file, err := os.Open(fullPath)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
info, err := file.Stat()
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
if info.IsDir() {
|
|
downloadDirectory(c, path)
|
|
return
|
|
}
|
|
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", info.Name()))
|
|
c.Header("Content-Type", "application/octet-stream")
|
|
c.Header("Content-Length", fmt.Sprintf("%d", info.Size()))
|
|
io.Copy(c.Writer, file)
|
|
}
|
|
|
|
func downloadDirectory(c *gin.Context, path string) {
|
|
fullPath := filepath.Join(rootDir, path)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"})
|
|
return
|
|
}
|
|
|
|
dirName := filepath.Base(path)
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s.zip", dirName))
|
|
c.Header("Content-Type", "application/zip")
|
|
c.Header("Content-Transfer-Encoding", "binary")
|
|
|
|
zw := zip.NewWriter(c.Writer)
|
|
defer zw.Close()
|
|
|
|
filepath.Walk(fullPath, func(filePath string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(rootDir, filePath)
|
|
if relPath == "." {
|
|
return nil
|
|
}
|
|
|
|
header, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header.Name = relPath
|
|
header.Method = zip.Deflate
|
|
|
|
if info.IsDir() {
|
|
header.Name += "/"
|
|
} else {
|
|
writer, err := zw.CreateHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !info.IsDir() {
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
io.Copy(writer, file)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func downloadMultiple(c *gin.Context, paths []string) {
|
|
c.Header("Content-Disposition", "attachment; filename=files.zip")
|
|
c.Header("Content-Type", "application/zip")
|
|
c.Header("Content-Transfer-Encoding", "binary")
|
|
|
|
zw := zip.NewWriter(c.Writer)
|
|
defer zw.Close()
|
|
|
|
for _, path := range paths {
|
|
fullPath := filepath.Join(rootDir, path)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
continue
|
|
}
|
|
|
|
info, err := os.Stat(fullPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if info.IsDir() {
|
|
filepath.Walk(fullPath, func(filePath string, fileInfo os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
relPath, _ := filepath.Rel(rootDir, filePath)
|
|
header, err := zip.FileInfoHeader(fileInfo)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
header.Name = relPath
|
|
header.Method = zip.Deflate
|
|
|
|
if fileInfo.IsDir() {
|
|
header.Name += "/"
|
|
} else {
|
|
writer, err := zw.CreateHeader(header)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer file.Close()
|
|
io.Copy(writer, file)
|
|
}
|
|
return nil
|
|
})
|
|
} else {
|
|
file, err := os.Open(fullPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
defer file.Close()
|
|
|
|
header, err := zip.FileInfoHeader(info)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
header.Name = filepath.Base(path)
|
|
header.Method = zip.Deflate
|
|
|
|
writer, err := zw.CreateHeader(header)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
io.Copy(writer, file)
|
|
}
|
|
}
|
|
}
|
|
|
|
func uploadFile(c *gin.Context) {
|
|
path := c.DefaultQuery("path", "")
|
|
if path == "" {
|
|
path = "."
|
|
}
|
|
|
|
dzuuid := c.PostForm("dzuuid")
|
|
chunkIndex := c.PostForm("dzchunkindex")
|
|
totalChunkCount := c.PostForm("dztotalchunkcount")
|
|
fileName := c.PostForm("dzanonymousfilename")
|
|
|
|
isChunked := dzuuid != "" && chunkIndex != ""
|
|
|
|
tmpDir := filepath.Join(rootDir, ".tmp", "chunks")
|
|
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
file, header, err := c.Request.FormFile("file")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
targetFileName := header.Filename
|
|
if fileName != "" {
|
|
targetFileName = fileName
|
|
}
|
|
|
|
fullPath := filepath.Join(rootDir, path, targetFileName)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: "无效的路径",
|
|
})
|
|
return
|
|
}
|
|
|
|
if isChunked {
|
|
idx := 0
|
|
fmt.Sscanf(chunkIndex, "%d", &idx)
|
|
total := 0
|
|
if totalChunkCount != "" {
|
|
fmt.Sscanf(totalChunkCount, "%d", &total)
|
|
}
|
|
|
|
chunkDir := filepath.Join(tmpDir, dzuuid)
|
|
if err := os.MkdirAll(chunkDir, 0755); err != nil {
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("chunk_%d", idx))
|
|
outFile, err := os.Create(chunkPath)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
defer outFile.Close()
|
|
|
|
io.Copy(outFile, file)
|
|
|
|
if idx == total-1 && total > 0 {
|
|
finalFile, err := os.Create(fullPath)
|
|
if err != nil {
|
|
os.RemoveAll(chunkDir)
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
defer finalFile.Close()
|
|
|
|
for i := 0; i < total; i++ {
|
|
chunkPath := filepath.Join(chunkDir, fmt.Sprintf("chunk_%d", i))
|
|
chunkFile, err := os.Open(chunkPath)
|
|
if err != nil {
|
|
finalFile.Close()
|
|
os.RemoveAll(chunkDir)
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
io.Copy(finalFile, chunkFile)
|
|
chunkFile.Close()
|
|
os.Remove(chunkPath)
|
|
}
|
|
os.RemoveAll(chunkDir)
|
|
|
|
watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: targetFileName}
|
|
|
|
c.JSON(http.StatusOK, UploadResponse{
|
|
Success: true,
|
|
Message: "上传成功",
|
|
})
|
|
} else {
|
|
c.JSON(http.StatusOK, UploadResponse{
|
|
Success: true,
|
|
Message: "分块上传成功",
|
|
})
|
|
}
|
|
} else {
|
|
outFile, err := os.Create(fullPath)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, UploadResponse{
|
|
Success: false,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
defer outFile.Close()
|
|
|
|
io.Copy(outFile, file)
|
|
|
|
watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: targetFileName}
|
|
|
|
c.JSON(http.StatusOK, UploadResponse{
|
|
Success: true,
|
|
Message: "上传成功",
|
|
})
|
|
}
|
|
}
|
|
|
|
func deleteFiles(c *gin.Context) {
|
|
var paths []string
|
|
if err := c.ShouldBindJSON(&paths); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"})
|
|
return
|
|
}
|
|
|
|
for _, path := range paths {
|
|
fullPath := filepath.Join(rootDir, path)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
continue
|
|
}
|
|
|
|
info, err := os.Stat(fullPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if info.IsDir() {
|
|
os.RemoveAll(fullPath)
|
|
} else {
|
|
os.Remove(fullPath)
|
|
}
|
|
|
|
watchChan <- WatchEvent{Type: "delete", Path: fullPath, Name: filepath.Base(path)}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
func moveFile(c *gin.Context) {
|
|
var req MoveRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"})
|
|
return
|
|
}
|
|
|
|
srcPath := filepath.Join(rootDir, req.SrcPath)
|
|
destPath := filepath.Join(rootDir, req.DestPath)
|
|
|
|
if !strings.HasPrefix(srcPath, rootDir) || !strings.HasPrefix(destPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"})
|
|
return
|
|
}
|
|
|
|
if err := os.Rename(srcPath, destPath); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
watchChan <- WatchEvent{Type: "move", Path: destPath, Name: filepath.Base(destPath)}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
func renameFile(c *gin.Context) {
|
|
var req RenameRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"})
|
|
return
|
|
}
|
|
|
|
srcPath := filepath.Join(rootDir, req.OldPath)
|
|
newPath := filepath.Join(rootDir, filepath.Dir(req.OldPath), req.NewName)
|
|
|
|
if !strings.HasPrefix(srcPath, rootDir) || !strings.HasPrefix(newPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"})
|
|
return
|
|
}
|
|
|
|
if err := os.Rename(srcPath, newPath); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
watchChan <- WatchEvent{Type: "rename", Path: newPath, Name: req.NewName}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
func createDir(c *gin.Context) {
|
|
var req CreateDirRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的请求体"})
|
|
return
|
|
}
|
|
|
|
fullPath := filepath.Join(rootDir, req.Path, req.Name)
|
|
if !strings.HasPrefix(fullPath, rootDir) {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: "无效的路径"})
|
|
return
|
|
}
|
|
|
|
if err := os.MkdirAll(fullPath, 0755); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
|
return
|
|
}
|
|
|
|
watchChan <- WatchEvent{Type: "create", Path: fullPath, Name: req.Name}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true})
|
|
}
|
|
|
|
func watchFiles(c *gin.Context) {
|
|
c.Header("Content-Type", "text/event-stream")
|
|
c.Header("Cache-Control", "no-cache")
|
|
c.Header("Connection", "keep-alive")
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
|
|
c.Writer.Flush()
|
|
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case event := <-watchChan:
|
|
data, _ := json.Marshal(event)
|
|
c.SSEvent("message", string(data))
|
|
c.Writer.Flush()
|
|
case <-ticker.C:
|
|
c.SSEvent("ping", time.Now().Format("2006-01-02 15:04:05"))
|
|
c.Writer.Flush()
|
|
case <-c.Request.Context().Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func startWatcher() {
|
|
var err error
|
|
watcher, err = fsnotify.NewWatcher()
|
|
if err != nil {
|
|
fmt.Printf("警告: 无法创建文件监控器: %v\n", err)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case event, ok := <-watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if event.Op&fsnotify.Create == fsnotify.Create ||
|
|
event.Op&fsnotify.Write == fsnotify.Write ||
|
|
event.Op&fsnotify.Remove == fsnotify.Remove ||
|
|
event.Op&fsnotify.Rename == fsnotify.Rename ||
|
|
event.Op&fsnotify.Chmod == fsnotify.Chmod {
|
|
|
|
relPath, _ := filepath.Rel(rootDir, event.Name)
|
|
if relPath == "." {
|
|
continue
|
|
}
|
|
|
|
eventType := "modify"
|
|
if event.Op&fsnotify.Create == fsnotify.Create {
|
|
eventType = "create"
|
|
} else if event.Op&fsnotify.Remove == fsnotify.Remove {
|
|
eventType = "delete"
|
|
} else if event.Op&fsnotify.Rename == fsnotify.Rename {
|
|
eventType = "rename"
|
|
}
|
|
|
|
select {
|
|
case watchChan <- WatchEvent{Type: eventType, Path: event.Name, Name: filepath.Base(event.Name)}:
|
|
default:
|
|
}
|
|
}
|
|
case err, ok := <-watcher.Errors:
|
|
if !ok {
|
|
return
|
|
}
|
|
fmt.Printf("监控错误: %v\n", err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
watcher.Add(rootDir)
|
|
filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
|
if err == nil && info.IsDir() {
|
|
watcher.Add(path)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func main() {
|
|
port := "8080"
|
|
dir := "./files"
|
|
|
|
for i := 1; i < len(os.Args); i++ {
|
|
switch os.Args[i] {
|
|
case "-port":
|
|
if i+1 < len(os.Args) {
|
|
port = os.Args[i+1]
|
|
i++
|
|
}
|
|
case "-root":
|
|
if i+1 < len(os.Args) {
|
|
dir = os.Args[i+1]
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
if dir == "" {
|
|
dir = "."
|
|
}
|
|
|
|
if !filepath.IsAbs(dir) {
|
|
cwd, _ := os.Getwd()
|
|
dir = filepath.Join(cwd, dir)
|
|
}
|
|
|
|
rootDir = dir
|
|
|
|
if err := os.MkdirAll(rootDir, 0755); err != nil {
|
|
fmt.Printf("错误: 无法创建根目录: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
gin.SetMode(gin.ReleaseMode)
|
|
router := gin.New()
|
|
router.Use(gin.Recovery())
|
|
router.Use(corsMiddleware())
|
|
|
|
router.GET("/", func(c *gin.Context) {
|
|
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
c.Header("X-Content-Type-Options", "nosniff")
|
|
c.Header("X-Frame-Options", "DENY")
|
|
c.Header("X-XSS-Protection", "1; mode=block")
|
|
|
|
html, err := os.ReadFile("/var/www/static/index.html")
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, "无法加载页面")
|
|
return
|
|
}
|
|
c.String(http.StatusOK, "%s", html)
|
|
})
|
|
|
|
router.Static("/static", "/var/www/static")
|
|
|
|
api := router.Group("/api")
|
|
{
|
|
api.GET("/files", listFiles)
|
|
api.GET("/preview", previewFile)
|
|
api.GET("/download", downloadFile)
|
|
api.POST("/upload", uploadFile)
|
|
api.DELETE("/files", deleteFiles)
|
|
api.PUT("/move", moveFile)
|
|
api.PUT("/rename", renameFile)
|
|
api.POST("/dir", createDir)
|
|
api.GET("/watch", watchFiles)
|
|
}
|
|
|
|
go startWatcher()
|
|
|
|
addr := fmt.Sprintf(":%s", port)
|
|
fmt.Printf("文件管理器已启动: http://localhost:%s\n", port)
|
|
fmt.Printf("根目录: %s\n", rootDir)
|
|
|
|
if err := router.Run(addr); err != nil {
|
|
fmt.Printf("错误: %v\n", err)
|
|
}
|
|
}
|
|
|
|
func corsMiddleware() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Header("Access-Control-Allow-Origin", "*")
|
|
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
|
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|