使用Golang的fs.FS递归遍历常规目录和zip文件的方法

使用Golang的fs.FS递归遍历常规目录和zip文件的方法 我希望使用 fs.FS 来遍历文件系统,打印出它查看的每个目录的完整名称,并且能够继续查看 zip 文件。同样,需要打印出 zip 文件所在位置的完整目录以及 zip 文件内部的目录位置。已经尝试了很多 fs.FS 的方法,但无法弄清楚如何跳转到读取 zip 文件,然后再返回到常规文件系统。

最终希望尝试创建一个类似 grep 的程序,能够搜索所有文件,即使它们位于 zip 文件中。

有没有人拥有可以无缝遍历常规文件系统并继续进入 zip 文件的代码?

我是一名退休的数据库管理员和程序员。

1 回复

更多关于使用Golang的fs.FS递归遍历常规目录和zip文件的方法的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


以下是一个使用 fs.FS 递归遍历常规目录和 ZIP 文件的完整示例。该代码通过自定义 fs.FS 实现来无缝处理 ZIP 文件,并打印所有目录的完整路径。

package main

import (
    "archive/zip"
    "fmt"
    "io/fs"
    "os"
    "path/filepath"
    "strings"
)

// ZipFS 包装 zip.Reader 实现 fs.FS 接口
type ZipFS struct {
    *zip.Reader
    basePath string // ZIP 文件在外部文件系统中的路径
}

func (z ZipFS) Open(name string) (fs.File, error) {
    return z.Reader.Open(name)
}

// 递归遍历函数
func walkFS(fsys fs.FS, basePath string, depth int) error {
    return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }

        // 构建完整路径
        fullPath := filepath.Join(basePath, path)
        
        if d.IsDir() {
            // 打印目录路径
            indent := strings.Repeat("  ", depth)
            fmt.Printf("%s[DIR] %s\n", indent, fullPath)
            
            // 如果是 ZIP 文件且是第一次进入(path 为文件名时)
            if !d.IsDir() && strings.HasSuffix(path, ".zip") {
                // 打开 ZIP 文件
                file, err := fsys.Open(path)
                if err != nil {
                    return err
                }
                defer file.Close()
                
                // 获取文件信息以读取内容
                fi, err := file.Stat()
                if err != nil {
                    return err
                }
                
                // 读取 ZIP 文件
                zr, err := zip.NewReader(file.(*os.File), fi.Size())
                if err != nil {
                    return err
                }
                
                // 创建 ZIP 的 FS 并递归遍历
                zipFS := ZipFS{zr, fullPath}
                return walkFS(zipFS, fullPath, depth+1)
            }
        } else {
            // 打印文件路径
            indent := strings.Repeat("  ", depth)
            fmt.Printf("%s[FILE] %s\n", indent, fullPath)
            
            // 如果是 ZIP 文件,进入并遍历
            if strings.HasSuffix(path, ".zip") {
                // 打开 ZIP 文件
                file, err := fsys.Open(path)
                if err != nil {
                    return err
                }
                defer file.Close()
                
                // 获取文件信息
                fi, err := file.Stat()
                if err != nil {
                    return err
                }
                
                // 对于 os.File 类型的处理
                if osFile, ok := file.(*os.File); ok {
                    zr, err := zip.NewReader(osFile, fi.Size())
                    if err != nil {
                        return err
                    }
                    
                    // 创建 ZIP 的 FS 并递归遍历
                    zipFS := ZipFS{zr, fullPath}
                    return walkFS(zipFS, fullPath+"/", depth+1)
                }
            }
        }
        return nil
    })
}

// 主函数:从当前目录开始遍历
func main() {
    cwd, err := os.Getwd()
    if err != nil {
        panic(err)
    }
    
    // 使用 os.DirFS 创建根文件系统
    rootFS := os.DirFS(cwd)
    
    fmt.Println("开始遍历文件系统(包含ZIP文件):")
    fmt.Println("======================================")
    
    if err := walkFS(rootFS, cwd, 0); err != nil {
        fmt.Printf("遍历错误: %v\n", err)
    }
}

对于更完整的实现,这里是一个增强版,包含更好的错误处理和 ZIP 文件检测:

package main

import (
    "archive/zip"
    "fmt"
    "io"
    "io/fs"
    "os"
    "path/filepath"
    "strings"
)

// 增强版 ZipFS,支持完整的 fs.FS 接口
type EnhancedZipFS struct {
    reader *zip.Reader
    prefix string
}

func (e EnhancedZipFS) Open(name string) (fs.File, error) {
    // 确保路径格式正确
    name = strings.TrimPrefix(name, "./")
    return e.reader.Open(name)
}

// 通用遍历函数
func traverseFileSystem(fsys fs.FS, basePath string, depth int) error {
    return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }

        fullPath := filepath.Join(basePath, path)
        indent := strings.Repeat("│   ", depth)
        
        if d.IsDir() {
            fmt.Printf("%s├── 📁 %s/\n", indent, path)
        } else {
            fmt.Printf("%s├── 📄 %s\n", indent, path)
            
            // 检查是否为 ZIP 文件
            if strings.HasSuffix(strings.ToLower(path), ".zip") {
                // 打开文件
                file, err := fsys.Open(path)
                if err != nil {
                    return err
                }
                
                // 尝试转换为 os.File 或读取全部内容
                var zipReader *zip.Reader
                if osFile, ok := file.(*os.File); ok {
                    // 如果是 os.File,可以直接使用
                    stat, _ := osFile.Stat()
                    zipReader, err = zip.NewReader(osFile, stat.Size())
                    file.Close()
                } else {
                    // 如果不是 os.File,需要读取到内存
                    defer file.Close()
                    tempFile, err := os.CreateTemp("", "zipfs-*.zip")
                    if err != nil {
                        return err
                    }
                    defer os.Remove(tempFile.Name())
                    defer tempFile.Close()
                    
                    _, err = io.Copy(tempFile, file)
                    if err != nil {
                        return err
                    }
                    
                    tempFile.Seek(0, 0)
                    stat, _ := tempFile.Stat()
                    zipReader, err = zip.NewReader(tempFile, stat.Size())
                }
                
                if err != nil {
                    return fmt.Errorf("读取ZIP文件失败 %s: %v", path, err)
                }
                
                // 递归遍历 ZIP 内容
                zipFS := EnhancedZipFS{reader: zipReader, prefix: fullPath}
                fmt.Printf("%s│   └── 📦 进入ZIP文件:\n", indent)
                return traverseFileSystem(zipFS, fullPath+"/", depth+1)
            }
        }
        return nil
    })
}

func main() {
    // 使用当前目录
    root := "."
    absRoot, _ := filepath.Abs(root)
    
    fmt.Printf("遍历根目录: %s\n", absRoot)
    fmt.Println("══════════════════════════════════════")
    
    fsys := os.DirFS(root)
    if err := traverseFileSystem(fsys, absRoot, 0); err != nil {
        fmt.Printf("错误: %v\n", err)
    }
}

要创建一个类似 grep 的程序,可以修改遍历函数来搜索文件内容:

// 搜索函数示例
func searchInFS(fsys fs.FS, basePath string, searchTerm string) error {
    return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
        if err != nil {
            return err
        }
        
        if !d.IsDir() {
            // 跳过 ZIP 文件(它们会被单独处理)
            if strings.HasSuffix(strings.ToLower(path), ".zip") {
                return processZipFile(fsys, path, basePath, searchTerm)
            }
            
            // 搜索文本文件内容
            if strings.HasSuffix(strings.ToLower(path), ".txt") ||
               strings.HasSuffix(strings.ToLower(path), ".go") ||
               strings.HasSuffix(strings.ToLower(path), ".md") {
                
                file, err := fsys.Open(path)
                if err != nil {
                    return err
                }
                defer file.Close()
                
                content, err := io.ReadAll(file)
                if err != nil {
                    return err
                }
                
                if strings.Contains(string(content), searchTerm) {
                    fullPath := filepath.Join(basePath, path)
                    fmt.Printf("找到匹配: %s\n", fullPath)
                }
            }
        }
        return nil
    })
}

func processZipFile(fsys fs.FS, zipPath, basePath, searchTerm string) error {
    // 打开并遍历 ZIP 文件的代码
    // 与上面示例类似,但调用 searchInFS 递归搜索
    return nil
}

这些示例展示了如何:

  1. 使用 fs.FS 接口统一处理常规文件和 ZIP 文件
  2. 递归遍历目录结构
  3. 正确处理 ZIP 文件内的路径
  4. 保持外部文件系统和 ZIP 内部文件系统的路径信息

关键点在于为 ZIP 文件实现 fs.FS 接口,然后递归调用相同的遍历函数。这样就能无缝地在常规文件系统和 ZIP 文件之间切换。

回到顶部