package file import ( "archive/tar" "archive/zip" "compress/gzip" "fmt" "io" "os" "path/filepath" "strings" "github.com/bodgit/sevenzip" "github.com/dsnet/compress/bzip2" ) // CompressionType 定义了支持的压缩文件类型枚举。 // 每个枚举值都是一个唯一的文件后缀。 type CompressionType string const ( ZIP CompressionType = ".zip" TAR CompressionType = ".tar" TARGZ CompressionType = ".tar.gz" TGZ CompressionType = ".tgz" TARBZ2 CompressionType = ".tar.bz2" TBZ2 CompressionType = ".tbz2" SEVEN_Z CompressionType = ".7z" ) // Matches 判断文件名是否以当前压缩类型定义的后缀结尾。 func (ct CompressionType) Matches(filename string) bool { return strings.HasSuffix(strings.ToLower(filename), string(ct)) } // DecompressAtomic 以原子方式将指定的压缩包解压到目标目录。 // 此函数是线程安全的,它会获取全局文件锁。 // // 核心保障:如果解压过程中发生任何错误,它会自动删除整个 destPath 目录以进行回滚。 // // sourcePath: 要解压的压缩包的完整路径。 // destPath: 目标解压目录。此函数将创建此目录,并将所有内容解压到其中。 // // 重要警告:请勿将一个已存在的、包含重要文件的目录作为 destPath。 // 此函数假定它拥有对 destPath 的完全控制权,并会在失败时将其彻底删除。 func DecompressAtomic(sourcePath, destPath string) error { action := func() error { return decompress(sourcePath, destPath) } onRollback := func(err error) { _ = os.RemoveAll(destPath) } return ExecuteWithLock(action, onRollback) } // decompress 是解压操作的核心实现,它本身不是线程安全的,也不提供回滚。 // 它应该被 DecompressAtomic 或其他调用方在 ExecuteWithLock 的回调中执行。 func decompress(sourcePath, destPath string) error { // 确保目标目录存在 if err := os.MkdirAll(destPath, 0755); err != nil { return fmt.Errorf("创建目标目录 %s 失败: %w", destPath, err) } // 直接在 switch 中使用 Matches 方法 switch { case ZIP.Matches(sourcePath): return decompressZip(sourcePath, destPath) case TAR.Matches(sourcePath), TARGZ.Matches(sourcePath), TGZ.Matches(sourcePath), TARBZ2.Matches(sourcePath), TBZ2.Matches(sourcePath): return decompressTar(sourcePath, destPath) case SEVEN_Z.Matches(sourcePath): return decompress7z(sourcePath, destPath) default: return fmt.Errorf("不支持的压缩文件类型: %s", sourcePath) } } // secureJoinPath 安全地将子路径连接到目标目录,并检查路径遍历攻击。 func secureJoinPath(destDir, subPath string) (string, error) { destFilePath := filepath.Join(destDir, subPath) if !strings.HasPrefix(destFilePath, filepath.Clean(destDir)+string(os.PathSeparator)) { return "", fmt.Errorf("检测到不安全的解压路径: %s", subPath) } return destFilePath, nil } // createFileFromReader 从一个 reader 创建并写入文件内容。 func createFileFromReader(filePath string, mode os.FileMode, reader io.Reader) error { // 确保父目录存在 if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { return fmt.Errorf("为文件 %s 创建父目录失败: %w", filePath, err) } // 创建目标文件 destFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) if err != nil { return fmt.Errorf("创建目标文件 %s 失败: %w", filePath, err) } defer destFile.Close() // 复制内容 if _, err = io.Copy(destFile, reader); err != nil { return fmt.Errorf("写入文件 %s 内容失败: %w", filePath, err) } return nil } // decompressZip 负责解压 ZIP 格式的压缩包。 func decompressZip(sourcePath, destPath string) error { reader, err := zip.OpenReader(sourcePath) if err != nil { return fmt.Errorf("打开 ZIP 文件 %s 失败: %w", sourcePath, err) } defer reader.Close() for _, f := range reader.File { destFilePath, err := secureJoinPath(destPath, f.Name) if err != nil { return err } if f.FileInfo().IsDir() { if err := os.MkdirAll(destFilePath, f.Mode()); err != nil { return fmt.Errorf("创建 ZIP 内目录 %s 失败: %w", destFilePath, err) } continue } srcFile, err := f.Open() if err != nil { return fmt.Errorf("打开 ZIP 内文件 %s 失败: %w", f.Name, err) } defer srcFile.Close() if err := createFileFromReader(destFilePath, f.Mode(), srcFile); err != nil { return err } } return nil } // processTarStream 是处理 tar 流的核心逻辑。 func processTarStream(tarReader *tar.Reader, destPath string) error { for { header, err := tarReader.Next() if err == io.EOF { break // 归档结束 } if err != nil { return fmt.Errorf("读取 tar 头部失败: %w", err) } destFilePath, err := secureJoinPath(destPath, header.Name) if err != nil { return err } switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(destFilePath, os.FileMode(header.Mode)); err != nil { return fmt.Errorf("创建 tar 内目录 %s 失败: %w", destFilePath, err) } case tar.TypeReg: if err := createFileFromReader(destFilePath, os.FileMode(header.Mode), tarReader); err != nil { return err } } } return nil } // decompressTar 负责解压 TAR 格式的归档文件。 func decompressTar(sourcePath, destPath string) error { file, err := os.Open(sourcePath) if err != nil { return fmt.Errorf("打开 tar 文件 %s 失败: %w", sourcePath, err) } defer file.Close() var streamReader io.Reader = file lowerPath := strings.ToLower(sourcePath) if TARGZ.Matches(lowerPath) || TGZ.Matches(lowerPath) { gzReader, err := gzip.NewReader(file) if err != nil { return fmt.Errorf("创建 gzip 读取器失败: %w", err) } defer gzReader.Close() streamReader = gzReader } else if TARBZ2.Matches(lowerPath) || TBZ2.Matches(lowerPath) { bz2Reader, err := bzip2.NewReader(file, nil) if err != nil { return fmt.Errorf("创建 bzip2 读取器失败: %w", err) } defer bz2Reader.Close() streamReader = bz2Reader } tarReader := tar.NewReader(streamReader) return processTarStream(tarReader, destPath) } // decompress7z 负责解压 7z 格式的压缩包。 func decompress7z(sourcePath, destPath string) error { reader, err := sevenzip.OpenReader(sourcePath) if err != nil { return fmt.Errorf("打开 7z 文件 %s 失败: %w", sourcePath, err) } defer reader.Close() for _, f := range reader.File { destFilePath, err := secureJoinPath(destPath, f.Name) if err != nil { return err } if f.FileInfo().IsDir() { if err := os.MkdirAll(destFilePath, f.Mode()); err != nil { return fmt.Errorf("创建 7z 内目录 %s 失败: %w", destFilePath, err) } continue } srcFile, err := f.Open() if err != nil { return fmt.Errorf("打开 7z 内文件 %s 失败: %w", f.Name, err) } defer srcFile.Close() if err := createFileFromReader(destFilePath, f.Mode(), srcFile); err != nil { return err } } return nil }