【问题标题】:missing files when extracting tarball in golang在golang中提取tarball时丢失文件
【发布时间】:2021-03-17 20:57:17
【问题描述】:

我正在尝试使用此功能在解压缩文件后解压缩文件,但是,当它解压缩时,缺少一些文件夹,我不知道为什么。当我通过 GUI 打开创建的 tarfile 时,UnGzip 工作正常,因此不包含该功能。

func main() {
fileUrl := "https://www.clamav.net/downloads/production/clamav-0.103.1.tar.gz"
filePath := "clamav-0.103.1.tar.gz"
tempFolder := "temp"
    
err := os.Mkdir(tempFolder, 0755)
if err != nil {
    panic(err)
}

err = DownloadFile(filePath, fileUrl)
if err != nil {
    panic(err)
}
fmt.Println("Downloaded: " + fileUrl)

UnGzip(filePath,tempFolder + "/clamav.tar")
UnTar(tempFolder + "/clamav.tar",tempFolder + "/clamAV/")
//err := os.RemoveAll("tempFolder")
//if err != nil {
    //panic(err)
//}

}

func UnTar(tarball, target string) error {
reader, err := os.Open(tarball)
if err != nil {
    return err
}
defer reader.Close()
tarReader := tar.NewReader(reader)

for {
    header, err := tarReader.Next()
    if err == io.EOF {
        break
    } else if err != nil {
        return err
    }

    path := filepath.Join(target, header.Name)
    info := header.FileInfo()
    if info.IsDir() {
        if err = os.MkdirAll(path, info.Mode()); err != nil {
            return err
        }
        continue
    }

    file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
    if err != nil {
        return err
    }
    defer file.Close()
    _, err = io.Copy(file, tarReader)
    if err != nil {
        return err
    }
}
return nil

}

这是我应该得到的:want 这就是我所拥有的:have

【问题讨论】:

  • 存档目录并不总是与文件分开列出。尝试在不存在的目录中创建文件时是否出现错误?
  • @jimB 没有错误,我确定目录存在
  • 我不明白,你说一些文件夹/目录丢失了——但你确定它们存在?
  • 我的错我以为你的意思是父目录,不应该使用 mkdirAll 创建文件夹吗? (我更新了问题以包含我获取的文件与我需要的文件的图片)如果文件试图安装在不存在的文件夹中存在问题,它不会创建任何文件夹而不是只获得一个几个文件夹,而且我仍然没有得到一些甚至不在父目录中的文件
  • 这个 tarball 似乎单独列出了所有目录,但您应该注意这一点。您没有检查从UnTar 返回的错误,它会告诉您这里出了什么问题。经常检查你的错误。

标签: ubuntu go gzip tar


【解决方案1】:

下面是一些示例代码:

package main

import (
   "archive/tar"
   "compress/gzip"
   "io"
   "os"
   "path"
)

func extract(source string) error {
   file, err := os.Open(source)
   if err != nil { return err }
   defer file.Close()
   gzRead, err := gzip.NewReader(file)
   if err != nil { return err }
   defer gzRead.Close()
   tarRead := tar.NewReader(gzRead)
   for {
      cur, err := tarRead.Next()
      if err == io.EOF { break } else if err != nil { return err }
      os.MkdirAll(path.Dir(cur.Name), os.ModeDir)
      switch cur.Typeflag {
      case tar.TypeReg:
         create, err := os.Create(cur.Name)
         if err != nil { return err }
         defer create.Close()
         create.ReadFrom(tarRead)
      case tar.TypeLink:
         os.Link(cur.Linkname, cur.Name)
      }
   }
   return nil
}

用法:

package main

func main() {
   extract("clamav-0.103.1.tar.gz")
}

【讨论】:

    【解决方案2】:

    对于每个进程允许的打开文件数,您可能会遇到ulimit。使用-a 标志运行ulimit,我认为默认open files 限制为1024。tarball 有2758 个文件。

    这是因为您在处理 tarReader 的 for 循环中推迟了文件描述符的关闭。

    要修复它,请在处理完每个文件后关闭它们:

    func UnTar(tarball, target string) error {
        reader, err := os.Open(tarball)
        if err != nil {
            return err 
        }   
        defer reader.Close()
        tarReader := tar.NewReader(reader)
    
        for {
            header, err := tarReader.Next()
            if err == io.EOF {
                break
            } else if err != nil {
                return err 
            }
    
            path := filepath.Join(target, header.Name)
            info := header.FileInfo()
            if info.IsDir() {
                if err = os.MkdirAll(path, info.Mode()); err != nil {
                    return err 
                }
                continue
            }
    
            err = processOneFile(tarReader, path, info.Mode())
            if err != nil {
                return err 
            }
        }   
        return nil 
    }
    
    func processOneFile(tarReader io.Reader, filePath string, fileMode os.FileMode) error {
        file, err := os.OpenFile(filePath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fileMode)
        if err != nil {
            return err 
        }   
        defer file.Close() // close error discarded
        _, err = io.Copy(file, tarReader)
        return err 
    }
    

    【讨论】:

      【解决方案3】:

      虽然关于ulimit 的其他答案已经非常好,但我只想补充两点:

      1. 您可以同时解压 gzip 和读取 tar 文件,而不是在两者之间创建临时文件。您也可以直接从 URL 流式传输文件并在下载时提取它
      2. 有人可能会创建一个恶意的 tar.gz 文件,该文件会使您的代码使用 zipslip 之类的东西覆盖重要文件(特别是如果您的程序以 root 身份运行,有人可能会注入 ../../../../etc/passwd 之类的文件路径,从而覆盖它文件,甚至可以编辑 crontab 文件并以这种方式执行代码?),您可能应该检查一下

      考虑到这一点,我们可以编写一个直接从io.Reader 提取的函数,该函数还检查目标目录之外的任何路径:

      // untargz decompresses a gzipped tar stream to the directory specified by target.
      // Note that `file` should be closed by the caller
      func untargz(file io.Reader, targetDir string) (err error) {
          gz, err := gzip.NewReader(file)
          if err != nil {
              return
          }
          // This does not close file
          defer gz.Close()
      
          tarReader := tar.NewReader(gz)
      
          for {
              header, err := tarReader.Next()
              if err == io.EOF {
                  break
              } else if err != nil {
                  return err
              }
      
              // This can be dangerous, similar to zipslip
              path := filepath.Join(targetDir, header.Name)
      
              // Check for ZipSlip. More Info: https://snyk.io/research/zip-slip-vulnerability#go
              if !strings.HasPrefix(path, filepath.Clean(targetDir)+string(os.PathSeparator)) {
                  err = fmt.Errorf("%s: illegal file path", path)
                  return err
              }
      
              info := header.FileInfo()
              if info.IsDir() {
                  if err = os.MkdirAll(path, info.Mode()); err != nil {
                      return err
                  }
                  continue
              }
      
              file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
              if err != nil {
                  return err
              }
      
              _, err = io.Copy(file, tarReader)
              if err != nil {
                  file.Close()
                  return err
              }
      
              err = file.Close()
              if err != nil {
                  return err
              }
          }
      
          return nil
      }
      

      考虑一下如果在该目录中的文件之后声明目录会发生什么(因为随后os.Create 失败)可能会有所帮助,但此函数不处理这种情况。

      此函数可用于直接流式传输到输出目录,但老实说,我不确定这是否是您想要的:

      func main() {
          resp, err := http.Get(`https://www.clamav.net/downloads/production/clamav-0.103.1.tar.gz`)
          if err != nil {
              panic(err)
          }
          defer resp.Body.Close()
      
          err = untargz(bufio.NewReader(resp.Body), "out")
          if err != nil {
              panic(err)
          }
      
          println("Done")
      }
      

      您可以找到完整的文件here

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2019-10-18
        • 1970-01-01
        • 1970-01-01
        • 2021-02-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多