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

391
internal/filesystem/zip.go Normal file
View File

@@ -0,0 +1,391 @@
package filesystem
import (
"archive/zip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
)
// ZipFileEntry 表示 zip 文件中的一个文件条目
type ZipFileEntry struct {
Name string
Path string // 在 zip 中的完整路径
Size int64
Modified string
IsDir bool
Method string // 压缩方法 (Store/Deflate)
}
// validateZipPath 验证 ZIP 文件路径是否有效
// 统一的路径验证逻辑,避免在多个函数中重复
func validateZipPath(zipPath string) error {
if !isSafePath(zipPath) {
return fmt.Errorf("zip 路径不安全")
}
// 检查 zip 文件是否存在
if _, err := os.Stat(zipPath); os.IsNotExist(err) {
return fmt.Errorf("zip 文件不存在")
}
return nil
}
// debugLog 条件日志记录,仅在调试模式下输出
// 通过设置环境变量 UDESK_ZIP_DEBUG=1 启用调试日志
var zipDebugMode = os.Getenv("UDESK_ZIP_DEBUG") == "1"
func debugLog(format string, args ...interface{}) {
if zipDebugMode {
log.Printf(format, args...)
}
}
// ListZipContents 列出 zip 文件内容
// 🔒 安全增强添加ZIP炸弹防护、路径遍历检查
func ListZipContents(zipPath string) ([]map[string]interface{}, error) {
debugLog("[ListZipContents] 开始处理 ZIP 文件: %s", zipPath)
// 统一验证路径
if err := validateZipPath(zipPath); err != nil {
debugLog("[ListZipContents] 路径验证失败: %v", err)
return nil, err
}
// 检查文件是否存在
fileInfo, err := os.Stat(zipPath)
if err != nil {
debugLog("[ListZipContents] 文件状态检查失败: %v", err)
return nil, fmt.Errorf("无法访问文件: %v", err)
}
debugLog("[ListZipContents] 文件信息: 大小=%d bytes, 权限=%v", fileInfo.Size(), fileInfo.Mode())
// 🔒 安全检查:检查文件大小(太小或太大)
if fileInfo.Size() < 22 {
debugLog("[ListZipContents] 文件太小,可能不是有效的 ZIP 文件: %d bytes", fileInfo.Size())
return nil, fmt.Errorf("文件太小 (%d bytes),可能不是有效的 ZIP 文件", fileInfo.Size())
}
// 🔒 安全检查ZIP炸弹防护检查文件大小
if fileInfo.Size() > MaxZipSize {
debugLog("[ListZipContents] ZIP文件过大: %d bytes", fileInfo.Size())
return nil, fmt.Errorf("ZIP文件过大 (%d bytes),超过限制 (%d bytes)", fileInfo.Size(), MaxZipSize)
}
// 检查文件是否可读
file, err := os.Open(zipPath)
if err != nil {
debugLog("[ListZipContents] 无法打开文件: %v", err)
return nil, fmt.Errorf("无法打开文件: %v", err)
}
// 读取前 4 个字节检查 ZIP 文件头
header := make([]byte, 4)
n, err := file.Read(header)
file.Close()
if err != nil || n != 4 {
debugLog("[ListZipContents] 无法读取文件头: n=%d, err=%v", n, err)
return nil, fmt.Errorf("无法读取文件头")
}
debugLog("[ListZipContents] 文件头: 0x%02x 0x%02x 0x%02x 0x%02x", header[0], header[1], header[2], header[3])
// ZIP 文件应该是 PK\x03\x04 或 PK\x05\x06 (空 ZIP)
if header[0] != 0x50 || header[1] != 0x4B { // 'P' 'K'
debugLog("[ListZipContents] 文件头签名错误,不是有效的 ZIP 文件")
return nil, fmt.Errorf("文件头签名错误,不是有效的 ZIP 文件 (可能是其他格式或已损坏)")
}
// 打开 zip 文件
debugLog("[ListZipContents] 尝试打开 ZIP 读取器...")
reader, err := zip.OpenReader(zipPath)
if err != nil {
debugLog("[ListZipContents] 打开 ZIP 失败: %v", err)
debugLog("[ListZipContents] 错误类型: %T", err)
// 提供更详细的错误信息和解决建议
errMsg := fmt.Sprintf("打开 zip 文件失败: %v", err)
if strings.Contains(err.Error(), "not a valid zip file") {
errMsg += "\n\n可能的原因:\n" +
"1. 文件已损坏或不完整\n" +
"2. 不是标准的 ZIP 格式\n" +
"3. 文件正在被其他程序占用(如压缩软件)\n" +
"4. 使用了特殊的压缩方式\n\n" +
"建议解决方法:\n" +
"- 关闭所有可能打开该文件的程序\n" +
"- 尝试用 7-Zip 或 WinRAR 重新压缩\n" +
"- 检查文件大小是否正常(不是 0 字节)\n" +
"- 如果是从网络下载的,尝试重新下载"
}
return nil, fmt.Errorf("%s", errMsg)
}
defer reader.Close()
debugLog("[ListZipContents] 成功打开 ZIP开始读取文件列表...")
// 🔒 安全检查ZIP炸弹防护检查解压后总大小
var totalUncompressed int64
for _, file := range reader.File {
totalUncompressed += int64(file.UncompressedSize64)
}
if totalUncompressed > MaxExtractSize {
debugLog("[ListZipContents] 解压后总大小过大: %d bytes", totalUncompressed)
return nil, fmt.Errorf("解压后总大小过大 (%d bytes),超过限制 (%d bytes)", totalUncompressed, MaxExtractSize)
}
var result []map[string]interface{}
fileCount := 0
dirCount := 0
// 遍历 zip 文件中的所有文件
for _, file := range reader.File {
// 跳过 macOS 资源分支文件
if strings.HasPrefix(file.Name, "__MACOSX/") {
continue
}
// 🔒 安全检查:路径遍历攻击防护
if strings.Contains(file.Name, "..") {
debugLog("[ListZipContents] 检测到路径遍历尝试: %s", file.Name)
return nil, fmt.Errorf("ZIP文件包含不安全的路径: %s", file.Name)
}
// 🔒 安全检查:绝对路径防护
if filepath.IsAbs(file.Name) {
debugLog("[ListZipContents] 检测到绝对路径: %s", file.Name)
return nil, fmt.Errorf("ZIP文件包含绝对路径: %s", file.Name)
}
isDir := file.Mode().IsDir()
name := filepath.Base(file.Name)
// 对于目录,使用目录名;对于文件,使用文件名
if isDir {
name = file.Name
dirCount++
} else {
fileCount++
}
// 压缩方法描述
method := "Store"
if file.Method == 8 {
method = "Deflate"
}
entry := map[string]interface{}{
"name": name,
"path": file.Name, // zip 中的完整路径
"is_dir": isDir,
"size": file.UncompressedSize64,
"compressed": file.CompressedSize64,
"mod_time": file.Modified.Format("2006-01-02 15:04:05"),
"method": method,
}
result = append(result, entry)
}
debugLog("[ListZipContents] 读取完成: %d 个文件, %d 个目录", fileCount, dirCount)
return result, nil
}
// ExtractFileFromZip 从 zip 文件中提取单个文件内容
// 优化:使用通用包装器,消除重复代码
func ExtractFileFromZip(zipPath, filePath string) (string, error) {
result, err := withZipFile(zipPath, filePath, func(file *zip.File) (interface{}, error) {
// 打开文件
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("打开 zip 中的文件失败: %v", err)
}
defer rc.Close()
// 读取内容
data, err := readAllFromFile(rc)
if err != nil {
return nil, fmt.Errorf("读取文件内容失败: %v", err)
}
return string(data), nil
})
if err != nil {
return "", err
}
return result.(string), nil
}
// ExtractFileFromZipToTemp 从 zip 文件中提取单个文件到临时目录
// 返回临时文件的完整路径
// 适用于提取图片等二进制文件
// 优化:使用通用包装器,消除重复代码
func ExtractFileFromZipToTemp(zipPath, filePath string) (string, error) {
debugLog("[ExtractFileFromZipToTemp] 开始提取: %s from %s", filePath, zipPath)
// 启动临时文件清理协程
go CleanOldTempFiles()
// 创建临时目录
tempDir := filepath.Join(os.TempDir(), TempFileDir)
if err := os.MkdirAll(tempDir, DefaultDirPermissions); err != nil {
return "", fmt.Errorf("创建临时目录失败: %v", err)
}
result, err := withZipFile(zipPath, filePath, func(file *zip.File) (interface{}, error) {
// 安全检查:文件大小限制
if file.UncompressedSize64 > MaxSingleFileSize {
debugLog("[ExtractFileFromZipToTemp] 文件过大: %d bytes", file.UncompressedSize64)
return nil, fmt.Errorf("文件过大 (%d bytes),超过限制 (%d bytes)",
file.UncompressedSize64, MaxSingleFileSize)
}
// 打开文件
rc, err := file.Open()
if err != nil {
return nil, fmt.Errorf("打开 zip 中的文件失败: %v", err)
}
defer rc.Close()
// 生成临时文件名
tempFileName := fmt.Sprintf("%d_%s", time.Now().UnixNano(), filepath.Base(file.Name))
tempFilePath := filepath.Join(tempDir, tempFileName)
// 创建临时文件
outFile, err := os.Create(tempFilePath)
if err != nil {
return nil, fmt.Errorf("创建临时文件失败: %v", err)
}
defer outFile.Close()
// 限制写入大小
limitedReader := &io.LimitedReader{R: rc, N: MaxSingleFileSize}
written, err := io.Copy(outFile, limitedReader)
if err != nil {
os.Remove(tempFilePath)
return nil, fmt.Errorf("写入临时文件失败: %v", err)
}
// 检查是否超过限制
if limitedReader.N <= 0 {
os.Remove(tempFilePath)
return nil, fmt.Errorf("文件大小超过限制")
}
debugLog("[ExtractFileFromZipToTemp] 提取成功: %s -> %s (%d bytes)",
file.Name, tempFilePath, written)
return tempFilePath, nil
})
if err != nil {
return "", err
}
return result.(string), nil
}
// CleanOldTempFiles 清理超过指定时间的临时文件
// 🔒 新增:防止临时文件累积占用磁盘空间
func CleanOldTempFiles() {
tempDir := filepath.Join(os.TempDir(), "u-desk-zip")
// 检查临时目录是否存在
dir, err := os.Open(tempDir)
if err != nil {
// 目录不存在或其他错误,无需清理
return
}
defer dir.Close()
// 读取目录内容
files, err := dir.Readdir(-1)
if err != nil {
return
}
cleanedCount := 0
now := time.Now()
for _, file := range files {
// 跳过目录
if file.IsDir() {
continue
}
// 检查文件年龄
if now.Sub(file.ModTime()) > TempFileCleanupAge {
filePath := filepath.Join(tempDir, file.Name())
if err := os.Remove(filePath); err == nil {
cleanedCount++
debugLog("[CleanOldTempFiles] 清理临时文件: %s (年龄: %v)", file.Name(), now.Sub(file.ModTime()))
}
}
}
if cleanedCount > 0 {
debugLog("[CleanOldTempFiles] 清理完成: 共清理 %d 个临时文件", cleanedCount)
}
}
// GetZipFileInfo 获取 zip 文件中特定文件的信息
// 优化:使用通用包装器,消除重复代码
func GetZipFileInfo(zipPath, filePath string) (map[string]interface{}, error) {
result, err := withZipFile(zipPath, filePath, func(file *zip.File) (interface{}, error) {
return createFileInfoMap(file, true), nil
})
if err != nil {
return nil, err
}
return result.(map[string]interface{}), nil
}
// validateZipFileBasic 验证ZIP文件的基本信息提取自ListZipContents
func validateZipFileBasic(zipPath string) error {
if err := validateZipPath(zipPath); err != nil {
return err
}
fileInfo, err := os.Stat(zipPath)
if err != nil {
return fmt.Errorf("无法访问文件: %v", err)
}
if fileInfo.Size() < MinValidZipSize {
return fmt.Errorf("文件太小 (%d bytes)", fileInfo.Size())
}
if fileInfo.Size() > MaxZipSize {
return fmt.Errorf("ZIP文件过大 (%d bytes)", fileInfo.Size())
}
return checkZipFileHeader(zipPath)
}
// checkZipFileHeader 检查ZIP文件头签名
func checkZipFileHeader(zipPath string) error {
file, err := os.Open(zipPath)
if err != nil {
return fmt.Errorf("无法打开文件: %v", err)
}
defer file.Close()
header := make([]byte, 4)
n, err := file.Read(header)
if err != nil || n != 4 {
return fmt.Errorf("无法读取文件头")
}
if header[0] != 0x50 || header[1] != 0x4B {
return fmt.Errorf("不是有效的 ZIP 文件")
}
return nil
}