Golang中如何创建包含目录的ZIP文件

Golang中如何创建包含目录的ZIP文件 大家好,

我正在尝试编写一个压缩目录为ZIP格式的小程序。 代码如下:

package main

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

var (
	writer *zip.Writer
)

func main() {
	fmt.Println("Hello from Dir Walker")

	args := os.Args[1:]
	if len(args) < 1 {
		log.Fatal("Missing input arguments")
	}

	f, err := os.Create(args[1])
	if err != nil {
		log.Fatal("Error:", err)
	}
	defer f.Close()

	writer = zip.NewWriter(f)
	defer writer.Close()

	start := time.Now()
	if err := filepath.WalkDir(args[0], walker); err != nil {
		log.Printf("Error in walker: %s\n", err)
	}
	t := time.Now()
	elapsed := t.Sub(start)
	fmt.Printf("Elapsed time: %v\n", elapsed)
}

func walker(path string, d fs.DirEntry, err error) error {
	if err != nil {
		return err
	}

	info, err := d.Info()
	if err != nil {
		return err
	}

	header, err := zip.FileInfoHeader(info)
	if err != nil {
		return err
	}

	header.Method = zip.Deflate
	header.Name = path

	hw, err := writer.CreateHeader(header)
	if err != nil {
		return err
	}

	if d.IsDir() || (d.Type()&fs.ModeSymlink != 0) {
		return nil
	}

	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	_, err = io.Copy(hw, f)
	return err
}

如果目录只包含文件,代码运行良好。 但当目录包含子目录时,我在命令行解压时会遇到错误:

» unzip ../output.zip                                                                               massimoc@massimo-mbp
Archive:  ../output.zip
  inflating: _                       
  inflating: a                       
checkdir error:  a exists but is not directory
                 unable to process a/b.
checkdir error:  a exists but is not directory
                 unable to process a/b/c.
checkdir error:  a exists but is not directory
                 unable to process a/b/c/d.
checkdir error:  a exists but is not directory
                 unable to process a/b/c/d/test.txt.
  inflating: cmd                     
checkdir error:  cmd exists but is not directory
                 unable to process cmd/zipper.
checkdir error:  cmd exists but is not directory
                 unable to process cmd/zipper/main.go.
  inflating: go.mod                  
  inflating: walk_dir                
  inflating: walk_dir.go             

看起来目录被添加了,但没有被标记为目录。

我对Go语言还比较陌生,所以我确信我遗漏了一些简单的东西。


更多关于Golang中如何创建包含目录的ZIP文件的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

我找到了解决方案,添加了

if d.IsDir() {
    header.Name += "/"
}

Create() 的文档说明

要创建目录而非文件,请在名称末尾添加斜杠。

CreateHeader() 的文档中并未提及这一点。

更多关于Golang中如何创建包含目录的ZIP文件的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题在于你的代码没有正确处理目录条目。当创建ZIP文件时,目录需要特殊的处理方式。以下是修复后的代码:

package main

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

func main() {
	fmt.Println("Hello from Dir Walker")

	args := os.Args[1:]
	if len(args) < 2 {
		log.Fatal("Usage: program <source_dir> <output_zip>")
	}

	srcDir := args[0]
	outputFile := args[1]

	f, err := os.Create(outputFile)
	if err != nil {
		log.Fatal("Error creating zip file:", err)
	}
	defer f.Close()

	writer := zip.NewWriter(f)
	defer writer.Close()

	start := time.Now()
	
	// 使用正确的相对路径处理
	err = filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}

		// 获取相对于源目录的路径
		relPath, err := filepath.Rel(srcDir, path)
		if err != nil {
			return err
		}

		// 跳过源目录本身
		if relPath == "." {
			return nil
		}

		// 确保目录路径以斜杠结尾
		if d.IsDir() {
			relPath += "/"
		}

		info, err := d.Info()
		if err != nil {
			return err
		}

		header, err := zip.FileInfoHeader(info)
		if err != nil {
			return err
		}

		// 设置正确的文件名(使用相对路径)
		header.Name = filepath.ToSlash(relPath)
		
		// 对于目录,设置正确的标志
		if d.IsDir() {
			header.Method = zip.Store
		} else {
			header.Method = zip.Deflate
		}

		hw, err := writer.CreateHeader(header)
		if err != nil {
			return err
		}

		// 如果是目录或符号链接,不需要写入内容
		if d.IsDir() || (d.Type()&fs.ModeSymlink != 0) {
			return nil
		}

		// 处理普通文件
		f, err := os.Open(path)
		if err != nil {
			return err
		}
		defer f.Close()

		_, err = io.Copy(hw, f)
		return err
	})

	if err != nil {
		log.Printf("Error walking directory: %s\n", err)
	}

	t := time.Now()
	elapsed := t.Sub(start)
	fmt.Printf("Elapsed time: %v\n", elapsed)
}

关键修改点:

  1. 目录路径处理:为目录路径添加斜杠后缀(relPath += "/"),这是ZIP格式中标识目录的标准方式。

  2. 相对路径:使用filepath.Rel()获取相对于源目录的路径,避免在ZIP文件中包含绝对路径。

  3. 目录压缩方法:为目录设置zip.Store方法(不压缩),因为目录条目没有实际内容需要压缩。

  4. 路径分隔符:使用filepath.ToSlash()确保使用正斜杠作为路径分隔符,这是ZIP文件的标准。

  5. 参数检查:添加了更清晰的参数检查和使用说明。

  6. 跳过源目录:添加逻辑跳过源目录本身(当relPath == "."时)。

这里还有一个更简洁的版本,使用archive/zip包的CreateRaw方法:

func walker(path string, d fs.DirEntry, err error) error {
	if err != nil {
		return err
	}

	// 获取相对路径
	relPath, err := filepath.Rel(srcDir, path)
	if err != nil {
		return err
	}
	
	if relPath == "." {
		return nil
	}

	// 转换为ZIP格式的路径(使用正斜杠)
	zipPath := filepath.ToSlash(relPath)
	
	// 如果是目录,确保以斜杠结尾
	if d.IsDir() {
		zipPath += "/"
	}

	info, err := d.Info()
	if err != nil {
		return err
	}

	// 使用Create而不是CreateHeader来简化处理
	if d.IsDir() {
		_, err := writer.Create(zipPath)
		return err
	}

	hw, err := writer.Create(zipPath)
	if err != nil {
		return err
	}

	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close()

	_, err = io.Copy(hw, f)
	return err
}

使用writer.Create()方法会自动处理文件头信息,包括正确设置目录标志。对于目录,只需要创建一个空的条目即可。

回到顶部