245 lines
6.0 KiB
Go
245 lines
6.0 KiB
Go
package controller
|
||
|
||
import (
|
||
"archive/zip"
|
||
"dashboard/utils"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type FileInfo struct {
|
||
Name string `json:"name"`
|
||
IsDir bool `json:"is_dir"`
|
||
Size int64 `json:"size"`
|
||
ModTime time.Time `json:"mod_time"`
|
||
}
|
||
|
||
func FileUploadHandle(c *gin.Context) error {
|
||
log, _ := utils.GetLogFromContext(c)
|
||
|
||
file, err := c.FormFile("file")
|
||
if err != nil {
|
||
log.Sugar().Errorf("Failed to get file: %v", err)
|
||
return err
|
||
}
|
||
|
||
targetDir := c.PostForm("target_dir")
|
||
if targetDir == "" {
|
||
log.Sugar().Errorf("Target directory not specified")
|
||
return errors.New("Target directory not specified")
|
||
}
|
||
|
||
// 防止路径穿越攻击
|
||
cleanDir := filepath.Clean(targetDir)
|
||
if strings.Contains(cleanDir, "..") || !strings.HasPrefix(cleanDir, "/") {
|
||
log.Sugar().Errorf("Invalid target directory: %s", cleanDir)
|
||
return errors.New("Invalid target directory")
|
||
}
|
||
|
||
// 限制文件大小(10MB)
|
||
const maxFileSize = 10 << 20 // 10MB
|
||
if file.Size > maxFileSize {
|
||
log.Sugar().Errorf("File too large: %d bytes", file.Size)
|
||
return errors.New("File size exceeds 10MB")
|
||
}
|
||
|
||
// 确保目标目录存在
|
||
if err := os.MkdirAll(cleanDir, 0755); err != nil {
|
||
log.Sugar().Errorf("Failed to create directory %s: %v", cleanDir, err)
|
||
return err
|
||
}
|
||
|
||
// 保存文件
|
||
targetPath := filepath.Join(cleanDir, file.Filename)
|
||
if err := c.SaveUploadedFile(file, targetPath); err != nil {
|
||
log.Sugar().Errorf("Failed to save file to %s: %v", targetPath, err)
|
||
return err
|
||
}
|
||
|
||
log.Sugar().Debug("File uploaded successfully to %s", targetPath)
|
||
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("File uploaded to %s", targetPath)})
|
||
|
||
return nil
|
||
}
|
||
|
||
func FileListHandle(c *gin.Context) error {
|
||
log, _ := utils.GetLogFromContext(c)
|
||
|
||
dirPath := c.Query("path")
|
||
if dirPath == "" {
|
||
log.Sugar().Errorf("Directory path not specified")
|
||
return errors.New("Directory path not specified")
|
||
}
|
||
|
||
// 清理路径并规范化
|
||
cleanPath := filepath.Clean(dirPath)
|
||
if !strings.HasPrefix(cleanPath, "/") {
|
||
log.Sugar().Errorf("Invalid directory path (not absolute): %s", cleanPath)
|
||
return errors.New("Invalid directory path")
|
||
}
|
||
|
||
// 防止路径穿越攻击
|
||
if strings.Contains(cleanPath, "/../") {
|
||
log.Sugar().Errorf("Invalid directory path (contains ..): %s", cleanPath)
|
||
return errors.New("Invalid directory path")
|
||
}
|
||
|
||
// 读取目录
|
||
entries, err := os.ReadDir(cleanPath)
|
||
if err != nil {
|
||
log.Sugar().Errorf("Failed to read directory %s: %v", cleanPath, err)
|
||
return err
|
||
}
|
||
|
||
var files []FileInfo
|
||
for _, entry := range entries {
|
||
info, err := entry.Info()
|
||
if err != nil {
|
||
log.Sugar().Errorf("Failed to get info for %s: %v", entry.Name(), err)
|
||
continue
|
||
}
|
||
files = append(files, FileInfo{
|
||
Name: entry.Name(),
|
||
IsDir: entry.IsDir(),
|
||
Size: info.Size(),
|
||
ModTime: info.ModTime(),
|
||
})
|
||
}
|
||
|
||
log.Sugar().Debug("Listed files in %s: %d entries", cleanPath, len(files))
|
||
c.JSON(http.StatusOK, gin.H{"files": files})
|
||
|
||
return nil
|
||
}
|
||
|
||
func FileDownloadHandle(c *gin.Context) error {
|
||
log, _ := utils.GetLogFromContext(c)
|
||
|
||
filePath := c.Query("path")
|
||
if filePath == "" {
|
||
log.Sugar().Errorf("File path not specified")
|
||
return errors.New("File path not specified")
|
||
}
|
||
|
||
// 清理路径并规范化
|
||
cleanPath := filepath.Clean(filePath)
|
||
if !strings.HasPrefix(cleanPath, "/") {
|
||
log.Sugar().Errorf("Invalid file path (not absolute): %s", cleanPath)
|
||
return errors.New("Invalid file path")
|
||
}
|
||
|
||
// 防止路径穿越攻击
|
||
if strings.Contains(cleanPath, "/../") {
|
||
log.Sugar().Errorf("Invalid file path (contains ..): %s", cleanPath)
|
||
return errors.New("Invalid file path")
|
||
}
|
||
|
||
// 检查路径是否存在
|
||
fileInfo, err := os.Stat(cleanPath)
|
||
if err != nil {
|
||
log.Sugar().Errorf("Path not found %s: %v", cleanPath, err)
|
||
return errors.New("Path not found")
|
||
}
|
||
|
||
if !fileInfo.IsDir() {
|
||
// 处理文件下载
|
||
const maxFileSize = 10 << 20 // 10MB
|
||
if fileInfo.Size() > maxFileSize {
|
||
log.Sugar().Errorf("File too large: %d bytes", fileInfo.Size())
|
||
return errors.New("File size exceeds 10MB")
|
||
}
|
||
|
||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath)))
|
||
c.Header("Content-Type", "application/octet-stream")
|
||
c.File(cleanPath)
|
||
return nil
|
||
}
|
||
|
||
// 处理目录下载(创建ZIP)
|
||
tmpFile, err := os.CreateTemp("", "dir-*.zip")
|
||
if err != nil {
|
||
log.Sugar().Errorf("Failed to create temp file: %v", err)
|
||
return errors.New("Failed to create zip file")
|
||
}
|
||
defer os.Remove(tmpFile.Name()) // 清理临时文件
|
||
|
||
zipWriter := zip.NewWriter(tmpFile)
|
||
totalSize := int64(0)
|
||
const maxZipSize = 10 << 20 // 10MB
|
||
|
||
err = filepath.Walk(cleanPath, func(path string, info os.FileInfo, err error) error {
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 创建ZIP中的文件头
|
||
header, err := zip.FileInfoHeader(info)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 设置ZIP中的相对路径
|
||
relPath, err := filepath.Rel(cleanPath, path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
header.Name = relPath
|
||
|
||
if info.IsDir() {
|
||
header.Name += "/"
|
||
} else {
|
||
header.Method = zip.Deflate
|
||
totalSize += info.Size()
|
||
if totalSize > maxZipSize {
|
||
return fmt.Errorf("directory size exceeds 10MB")
|
||
}
|
||
}
|
||
|
||
writer, err := zipWriter.CreateHeader(header)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if !info.IsDir() {
|
||
file, err := os.Open(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer file.Close()
|
||
_, err = io.Copy(writer, file)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
|
||
if err != nil {
|
||
zipWriter.Close()
|
||
log.Sugar().Errorf("Failed to create zip for %s: %v", cleanPath, err)
|
||
return err
|
||
}
|
||
|
||
if err := zipWriter.Close(); err != nil {
|
||
log.Sugar().Errorf("Failed to close zip writer: %v", err)
|
||
return err
|
||
}
|
||
|
||
// 设置下载头
|
||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filepath.Base(cleanPath)+".zip"))
|
||
c.Header("Content-Type", "application/zip")
|
||
c.File(tmpFile.Name())
|
||
log.Sugar().Debug("Directory %s downloaded as zip", cleanPath)
|
||
|
||
return nil
|
||
}
|