在 golang 中提取 tarball 时丢失文件
missing files when extracting tarball in golang
我正在尝试使用此功能在解压缩文件后解压缩文件,但是,解压缩时会丢失一些文件夹,我不知道为什么。当我通过 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
下面是一些示例代码:
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")
}
对于每个进程允许的打开文件数,您可能 运行 进入 ulimit
。 运行 ulimit
带有 -a
标志,我认为默认的 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
}
虽然关于 ulimit
的其他答案已经非常好,但我只想补充两件事:
- 您可以同时解压缩 gzip 和读取 tar 文件,而不是在两者之间创建临时文件。您也可以直接从 URL 流式传输文件并在下载时解压缩
- 有人可能会创建一个恶意的 tar.gz 文件,使您的代码使用 zipslip 之类的东西覆盖重要文件(特别是如果您的程序以 root 身份运行,有人可能会注入一个文件路径,例如
../../../../etc/passwd
并因此覆盖该文件,甚至可能编辑 crontab 文件并以这种方式执行代码?),您可能应该检查一下
考虑到这一点,我们可以编写一个直接从 io.Reader
中提取的函数,该函数还检查 target 目录之外的任何路径:
// 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。
我正在尝试使用此功能在解压缩文件后解压缩文件,但是,解压缩时会丢失一些文件夹,我不知道为什么。当我通过 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
下面是一些示例代码:
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")
}
对于每个进程允许的打开文件数,您可能 运行 进入 ulimit
。 运行 ulimit
带有 -a
标志,我认为默认的 open files
限制是 1024。tarball 有 2758 个文件。
这是因为您在处理 tarReader
.
要修复它,请关闭您处理过的每个文件:
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
}
虽然关于 ulimit
的其他答案已经非常好,但我只想补充两件事:
- 您可以同时解压缩 gzip 和读取 tar 文件,而不是在两者之间创建临时文件。您也可以直接从 URL 流式传输文件并在下载时解压缩
- 有人可能会创建一个恶意的 tar.gz 文件,使您的代码使用 zipslip 之类的东西覆盖重要文件(特别是如果您的程序以 root 身份运行,有人可能会注入一个文件路径,例如
../../../../etc/passwd
并因此覆盖该文件,甚至可能编辑 crontab 文件并以这种方式执行代码?),您可能应该检查一下
考虑到这一点,我们可以编写一个直接从 io.Reader
中提取的函数,该函数还检查 target 目录之外的任何路径:
// 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。