重构:文件系统模块化架构,优化应用启动流程
This commit is contained in:
391
internal/filesystem/zip.go
Normal file
391
internal/filesystem/zip.go
Normal 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user