尝试从文件和目录创建tar.gz文件时出现“写入过长”的错误

11

我正在尝试从多个目录和文件创建一个tar.gz文件。希望它的使用方式与以下命令相同:

tar -cvzf sometarfile.tar.gz somedir/ someotherdir/ somefile.json somefile.xml

假设这些目录里面还有其他的目录。

我有以下输入:

    paths := []string{
      "somedir/",
      "someotherdir/",
      "somefile.json",
      "somefile.xml",
    }

并使用这些:

    func TarFilesDirs(paths []string, tarFilePath string ) error {
       // set up the output file
       file, err := os.Create(tarFilePath)
       if err != nil {
           return err
       }

       defer file.Close()
       // set up the gzip writer
       gz := gzip.NewWriter(file)
       defer gz.Close()

       tw := tar.NewWriter(gz)
       defer tw.Close()

       // add each file/dir as needed into the current tar archive
       for _,i := range paths {
          if err := tarit(i, tw); err != nil {
               return err
          }
       }

       return nil
   }

func tarit(source string, tw *tar.Writer) error {
    info, err := os.Stat(source)
    if err != nil {
        return nil
    }

    var baseDir string
    if info.IsDir() {
        baseDir = filepath.Base(source)
    }

    return filepath.Walk(source,
        func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }

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

            if baseDir != "" {
                header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, source))
            }

            if err := tw.WriteHeader(header); err != nil {
                return err
            }

            if info.IsDir() {
                return nil
            }

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

            defer file.Close()

            _, err = io.Copy(tw, file)
            if err != nil {
                log.Println("failing here")
                return err
            }

            return err
        })
}

问题:如果目录很大,我会遇到以下情况:

archive/tar: write too long

错误,当我将其删除后一切都可以正常工作。

我已经没有更多的想法了,在这上面浪费了许多时间试图找到解决方案......

有什么想法吗?

谢谢

3个回答

14

我之前也遇到了类似的问题,直到我仔细查看了tar.FileInfoHeader文档:

FileInfoHeader从fi创建一个部分填充的Header。如果fi描述了符号链接,则FileInfoHeader将link记录为链接目标。如果fi描述了目录,则在名称后添加斜杠。因为os.FileInfo的Name方法仅返回它所描述的文件的基本名称,所以可能需要修改返回的头文件的Name字段以提供文件的完整路径名。

实际上,在使用WriteHeader写入之前,FileInfoHeader不能保证填充所有头字段。如果您查看实现,Size字段仅在regular文件上设置。您的代码片段似乎只处理目录,这意味着如果您遇到任何其他非常规文件,您将使用大小为零的头文件进行写入,然后尝试将磁盘上可能具有非零大小的特殊文件复制到tar中。Go会返回ErrWriteTooLong以防止您创建损坏的tar文件。

我想出了这个解决方案,自从使用它以来就没有再遇到过这个问题。

    if err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
        if err != nil {
            return check(err)
        }

        var link string
        if info.Mode()&os.ModeSymlink == os.ModeSymlink {
            if link, err = os.Readlink(path); err != nil {
                return check(err)
            }
        }

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

        header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, directory))
        if err = tw.WriteHeader(header); err != nil {
            return check(err)
        }

        if !info.Mode().IsRegular() { //nothing more to do for non-regular
            return nil
        }

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

        if _, err = io.CopyBuffer(tw, fh, buf); err != nil {
            return check(err)
        }
        return nil
})

1
文件在磁盘上可能已经发生更改并变得更长。 - dpington
请注意 - 一些文件系统在属性中没有准确列出文件大小,例如/ proc。当流氓存档尝试包括SELinux上下文的/proc/PID/attr/current时,我曾看到过这种情况。文件属性将始终报告大小为零,但读取会发现数据。在GoLang中,它不可避免地会生成“写入过长”的失败,而GNU tar / libz则没有问题。请注意要使用什么文件系统来存档以及其是否准确报告长度。 - BoeroBoy

1

由于您只在大目录中看到此问题,我认为以下修复措施可能没有帮助,但这将解决从可能不断增长的文件创建tar的问题。

在我的情况下,问题是当我们创建tar头时,tar.FileInfoHeader中的header.Size被设置为那个时间点的文件大小(info.Size())。

当我们稍后在代码中尝试打开相关文件(os.Open)并复制其内容(io.Copy)时,我们冒着比之前设置的tar头大小更多地复制数据的风险,因为文件可能已经在此期间增长。

此代码片段将确保我们仅复制与我们设置的tar头大小相同的数据:

_, err = io.**CopyN**(tw, file, info.Size())
if err != nil {
    log.Println("failing here")
    return err
}

0
Write 函数将数据写入 tar 存档中的当前条目。如果在 WriteHeader 后写入 hdr.Size 字节以上的内容,Write 将返回 ErrWriteTooLong 错误。
可以在头部添加 Size 选项,但我没试过,也许有帮助…
同时请参阅:https://golang.org/pkg/archive/tar/

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接