dashboard/controller/files.go
2025-05-21 11:26:54 +08:00

249 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = 100 << 20 // 100MB
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().Debugf("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().Debugf("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 = 100 << 20 // 100MB
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 = 100 << 20 // 100MB
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 100MB")
}
}
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().Debugf("Directory %s downloaded as zip", cleanPath)
return nil
}