Private
Public Access
1
0

重构:文件系统模块化架构,优化应用启动流程

This commit is contained in:
2026-01-28 00:28:54 +08:00
parent 4a9b25a505
commit 8c577f70e7
123 changed files with 32030 additions and 967 deletions

View File

@@ -0,0 +1,260 @@
package filesystem
import (
"encoding/base64"
"fmt"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
)
// LocalFileServer 本地文件服务器(独立的 HTTP 服务器)
type LocalFileServer struct {
server *http.Server
addr string
}
var (
localFileServer *LocalFileServer
localFileServerOnce sync.Once
)
// StartLocalFileServer 启动本地文件服务器
func StartLocalFileServer() (string, error) {
var initErr error
localFileServerOnce.Do(func() {
// 创建多路复用器
mux := http.NewServeMux()
// 注册 /localfs/ 路由
mux.HandleFunc("/localfs/", handleLocalFileRequest)
// 创建服务器(固定端口)
server := &http.Server{
Addr: "localhost:18765",
Handler: mux,
}
// 启动服务器
go func() {
log.Printf("[LocalFileServer] 正在启动...")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("[LocalFileServer] 启动失败: %v", err)
initErr = err
}
}()
localFileServer = &LocalFileServer{
server: server,
addr: "localhost:18765",
}
log.Printf("[LocalFileServer] 已启动,监听: %s", localFileServer.addr)
})
if localFileServer == nil {
return "", initErr
}
return localFileServer.addr, initErr
}
// handleLocalFileRequest 处理本地文件请求
func handleLocalFileRequest(w http.ResponseWriter, r *http.Request) {
// 只处理 GET 请求
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
log.Printf("[LocalFileHandler] 收到请求: %s", r.URL.Path)
// 从 URL 路径获取文件路径(移除 /localfs/ 前缀)
pathPart := strings.TrimPrefix(r.URL.Path, "/localfs/")
log.Printf("[LocalFileHandler] TrimPrefix 后: %s", pathPart)
if pathPart == "" || pathPart == r.URL.Path {
log.Printf("[LocalFileHandler] 路径前缀无效")
http.Error(w, "Invalid path. Use: /localfs/C:/path/to/file", http.StatusBadRequest)
return
}
// 🔒 修复先进行URL解码防止路径遍历攻击
decodedPath, err := url.QueryUnescape(pathPart)
if err != nil {
log.Printf("[LocalFileHandler] URL解码失败: %v", err)
http.Error(w, "Invalid path encoding", http.StatusBadRequest)
return
}
log.Printf("[LocalFileHandler] URL解码后: %s", decodedPath)
// 🔒 修复:在路径转换前检查是否包含危险字符
if strings.Contains(decodedPath, "..") {
log.Printf("[LocalFileHandler] 检测到路径遍历尝试")
http.Error(w, "Path traversal detected", http.StatusForbidden)
return
}
// 路径转换(统一使用反斜杠)
filePath := strings.ReplaceAll(decodedPath, "/", "\\")
filePath = filepath.Clean(filePath)
log.Printf("[LocalFileHandler] 最终路径: %s", filePath)
// 安全检查
if !isSafePath(filePath) {
log.Printf("[LocalFileHandler] 路径未通过安全检查: %s", filePath)
http.Error(w, "Unsafe path", http.StatusForbidden)
return
}
// 🔒 修复:文件类型白名单检查
ext := strings.ToLower(filepath.Ext(filePath))
if !isAllowedFileType(ext) {
log.Printf("[LocalFileHandler] 不允许的文件类型: %s", ext)
http.Error(w, fmt.Sprintf("Forbidden file type: %s", ext), http.StatusForbidden)
return
}
// 检查文件是否存在
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
log.Printf("[LocalFileHandler] 文件不存在: %s", filePath)
http.Error(w, fmt.Sprintf("File not found: %s", filePath), http.StatusNotFound)
} else {
log.Printf("[LocalFileHandler] 无法访问文件: %v", err)
http.Error(w, fmt.Sprintf("Failed to stat file: %v", err), http.StatusInternalServerError)
}
return
}
// 🔒 限制文件大小最大500MB
const maxFileSize = 500 * 1024 * 1024
if fileInfo.Size() > maxFileSize {
log.Printf("[LocalFileHandler] 文件过大: %d bytes", fileInfo.Size())
http.Error(w, "File too large", http.StatusForbidden)
return
}
// 打开文件
file, err := os.Open(filePath)
if err != nil {
log.Printf("[LocalFileHandler] 打开文件失败: %v", err)
http.Error(w, fmt.Sprintf("Failed to open file: %v", err), http.StatusInternalServerError)
return
}
defer file.Close()
// 设置响应头
contentType := getContentType(ext)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Cache-Control", "public, max-age=3600")
// 支持 Range 请求
w.Header().Set("Accept-Ranges", "bytes")
// 获取文件信息(用于 Range 请求)
fileStat, err := file.Stat()
if err != nil {
log.Printf("[LocalFileHandler] 获取文件信息失败: %v", err)
http.Error(w, fmt.Sprintf("Failed to stat file: %v", err), http.StatusInternalServerError)
return
}
// 使用 http.ServeContent 实现流式传输(支持 Range 请求)
http.ServeContent(w, r, filepath.Base(filePath), fileStat.ModTime(), file)
log.Printf("[LocalFileHandler] 文件传输成功: %s (%d bytes)", filePath, fileStat.Size())
}
// LocalFileHandler 本地文件处理器(兼容旧代码)
// 用于直接从文件系统提供文件,避免 base64 编码
type LocalFileHandler struct {
http.Handler
}
// NewLocalFileHandler 创建本地文件处理器
func NewLocalFileHandler() *LocalFileHandler {
// 启动本地文件服务器
go func() {
if _, err := StartLocalFileServer(); err != nil {
log.Printf("[LocalFileHandler] 启动本地文件服务器失败: %v", err)
}
}()
return &LocalFileHandler{}
}
// ServeHTTP 处理 HTTP 请求(代理到 handleLocalFileRequest
func (h *LocalFileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("[LocalFileHandler.ServeHTTP] 收到请求: %s (RawPath: %s)", r.URL.Path, r.URL.RawPath)
// 检查是否是 /localfs/ 请求
if !strings.HasPrefix(r.URL.Path, "/localfs/") {
log.Printf("[LocalFileHandler.ServeHTTP] 路径不匹配 /localfs/ 前缀返回404")
// 不是 /localfs/ 请求,返回 404
http.NotFound(w, r)
return
}
// 直接调用实际的请求处理器
handleLocalFileRequest(w, r)
}
// getContentType 根据文件扩展名返回 MIME 类型
// 使用统一的文件类型管理器
func getContentType(ext string) string {
return defaultFileTypeManager.GetMIMEType(ext)
}
// ReadFileAsBase64 读取文件并返回 base64 编码的字符串
// 用于读取从 ZIP 提取的临时图片文件
func ReadFileAsBase64(filePath string) (string, error) {
log.Printf("[ReadFileAsBase64] 读取文件: %s", filePath)
if !isSafePath(filePath) {
return "", fmt.Errorf("路径不安全")
}
// 检查文件是否存在
fileInfo, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
return "", fmt.Errorf("文件不存在: %s", filePath)
}
return "", fmt.Errorf("无法访问文件: %v", err)
}
log.Printf("[ReadFileAsBase64] 文件大小: %d bytes", fileInfo.Size())
// 读取文件
data, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("读取文件失败: %v", err)
}
// 编码为 base64
encoded := base64.StdEncoding.EncodeToString(data)
log.Printf("[ReadFileAsBase64] 编码成功: 原始=%d, base64=%d", len(data), len(encoded))
// 获取文件扩展名并确定 MIME 类型
ext := strings.ToLower(filepath.Ext(filePath))
mimeType := getContentType(ext)
// 返回 data URI 格式: data:image/png;base64,iVBORw0KG...
return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil
}
// HandleLocalFile 处理 /localfs/ 路由的 HTTP 请求
// 前端可以请求 http://localhost:18765/localfs/C:/path/to/image.jpg
// 注意:此函数与 ServeHTTP 功能重复,建议统一使用 ServeHTTP
func HandleLocalFile(w http.ResponseWriter, r *http.Request) {
handler := NewLocalFileHandler()
handler.ServeHTTP(w, r)
}
// isAllowedFileType 检查文件类型是否在白名单中
// 使用统一的文件类型管理器
func isAllowedFileType(ext string) bool {
return defaultFileTypeManager.IsAllowed(ext)
}