fix(file): 补全单文件格式压缩入口并适配压缩测试

.bz2/.xz/.zst 后缀已被识别为单文件压缩格式,但 Compress 未提供
对应入口导致报 unsupported format,与 .gz 一并收敛为统一分支,
解压侧同步收敛,测试改为表驱动覆盖归档与单文件两类格式

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
耗子
2026-07-18 18:07:05 +08:00
parent 1450c00ec6
commit b5bdfdb110
2 changed files with 80 additions and 164 deletions
+27 -25
View File
@@ -47,12 +47,12 @@ func Compress(dir string, src []string, dst string) error {
switch format {
case Zip:
_, err = shell.ExecfWithDir(dir, "zip -qr -o %s %s", dst, strings.Join(src, " "))
case Gz:
// gzip 仅支持压缩单个文件
case Gz, Bz2, Xz, Zst:
// 单文件压缩格式仅支持压缩单个文件
if len(src) != 1 {
return errors.New("gz format only supports compressing a single file")
return fmt.Errorf("%s format only supports compressing a single file", format)
}
_, err = shell.ExecfWithDir(dir, "gzip -c %s > %s", src[0], dst)
_, err = shell.ExecfWithDir(dir, "%s -c %s > %s", compressorByFormat(format), src[0], dst)
case TGz:
_, err = shell.ExecfWithDir(dir, "tar -czf %s %s", dst, strings.Join(src, " "))
case TBz2:
@@ -90,11 +90,10 @@ func UnCompress(src string, dst string) error {
case Zip:
// 用 7z 解压 zip,自动检测文件名编码,避免中文文件名变成 #Uxxxx
_, err = shell.Execf("7z x -y '%s' -o'%s'", src, dst)
case Gz:
// 单独的 gzip 文件(如 .sql.gz),解压到目标目录
// gunzip -k 保留原文件,-c 输出到标准输出,重定向到目标文件
baseName := strings.TrimSuffix(filepath.Base(src), ".gz")
_, err = shell.Execf("gunzip -c '%s' > '%s'", src, filepath.Join(dst, baseName))
case Gz, Bz2, Xz, Zst:
// 单独压缩的文件(如 .sql.gz),解压到目标目录
baseName := strings.TrimSuffix(filepath.Base(src), filepath.Ext(src))
_, err = shell.Execf("%s -dc '%s' > '%s'", compressorByFormat(format), src, filepath.Join(dst, baseName))
case TGz:
_, err = shell.Execf("tar -xzf '%s' -C '%s'", src, dst)
case TBz2:
@@ -103,22 +102,10 @@ func UnCompress(src string, dst string) error {
_, err = shell.Execf("tar -xf '%s' -C '%s'", src, dst)
case TXz:
_, err = shell.Execf("tar -xJf '%s' -C '%s'", src, dst)
case Xz:
// 单独的 xz 文件(如 .sql.xz),解压到目标目录
baseName := strings.TrimSuffix(filepath.Base(src), ".xz")
_, err = shell.Execf("xz -dc '%s' > '%s'", src, filepath.Join(dst, baseName))
case Bz2:
// 单独的 bzip2 文件(如 .sql.bz2),解压到目标目录
baseName := strings.TrimSuffix(filepath.Base(src), ".bz2")
_, err = shell.Execf("bzip2 -dc '%s' > '%s'", src, filepath.Join(dst, baseName))
case SevenZip:
_, err = shell.Execf("7z x -y '%s' -o'%s'", src, dst)
case TZst:
_, err = shell.Execf("tar --zstd -xf '%s' -C '%s'", src, dst)
case Zst:
// 单独的 zstd 文件(如 .sql.zst),解压到目标目录
baseName := strings.TrimSuffix(filepath.Base(src), ".zst")
_, err = shell.Execf("zstd -dc '%s' > '%s'", src, filepath.Join(dst, baseName))
default:
return errors.New("unsupported format")
}
@@ -145,12 +132,12 @@ func CompressShell(dir string, src []string, dst string) (string, error) {
switch format {
case Zip:
cmd = fmt.Sprintf("zip -qr -o '%s' %s", dst, sources)
case Gz:
// gzip 仅支持压缩单个文件
case Gz, Bz2, Xz, Zst:
// 单文件压缩格式仅支持压缩单个文件
if len(src) != 1 {
return "", errors.New("gz format only supports compressing a single file")
return "", fmt.Errorf("%s format only supports compressing a single file", format)
}
cmd = fmt.Sprintf("gzip -c %s > '%s'", sources, dst)
cmd = fmt.Sprintf("%s -c %s > '%s'", compressorByFormat(format), sources, dst)
case TGz:
cmd = fmt.Sprintf("tar -czf '%s' %s", dst, sources)
case TBz2:
@@ -245,6 +232,21 @@ func ListCompress(src string) ([]string, error) {
return strings.Split(out, "\n"), nil
}
// compressorByFormat 单文件压缩格式对应的压缩工具,压缩用 -c,解压用 -dc
func compressorByFormat(format FormatArchive) string {
switch format {
case Gz:
return "gzip"
case Bz2:
return "bzip2"
case Xz:
return "xz"
case Zst:
return "zstd"
}
return ""
}
// formatArchiveByPath 根据文件后缀获取压缩格式
func formatArchiveByPath(path string) (FormatArchive, error) {
switch filepath.Ext(path) {
+53 -139
View File
@@ -3,6 +3,7 @@ package io
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/libtnb/utils/env"
@@ -52,164 +53,77 @@ func (s *IOTestSuite) TestWriteAppendAppendsToFile() {
s.Equal("Hello, World!", content)
}
// archiveExts 归档格式,支持多文件压缩
var archiveExts = []string{".zip", ".tar", ".tar.gz", ".tgz", ".tar.bz2", ".tar.xz", ".tar.zst", ".7z"}
// singleExts 单文件压缩格式,仅支持压缩单个文件
var singleExts = []string{".gz", ".bz2", ".xz", ".zst"}
func (s *IOTestSuite) TestCompress() {
abs, err := filepath.Abs("testdata")
s.NoError(err)
src := []string{"compress_test1.txt", "compress_test2.txt"}
err = Write(filepath.Join(abs, src[0]), "File 1", 0644)
s.NoError(err)
err = Write(filepath.Join(abs, src[1]), "File 2", 0644)
s.NoError(err)
s.NoError(Write(filepath.Join(abs, src[0]), "File 1", 0644))
s.NoError(Write(filepath.Join(abs, src[1]), "File 2", 0644))
err = Compress(abs, src, filepath.Join(abs, "compress_test.zip"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "compress_test.bz2"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "compress_test.tar"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "compress_test.tar.gz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "compress_test.tgz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "compress_test.xz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "compress_test.7z"))
s.NoError(err)
s.NoError(Remove("testdata"))
for _, ext := range archiveExts {
s.NoError(Compress(abs, src, filepath.Join(abs, "compress_test"+ext)), ext)
}
for _, ext := range singleExts {
s.NoError(Compress(abs, src[:1], filepath.Join(abs, "compress_single"+ext)), ext)
s.Error(Compress(abs, src, filepath.Join(abs, "compress_multi"+ext)), ext)
}
}
func (s *IOTestSuite) TestUnCompress() {
abs, err := filepath.Abs("testdata")
s.NoError(err)
src := []string{"uncompress_test1.txt", "uncompress_test2.txt"}
err = Write(filepath.Join(abs, src[0]), "File 1", 0644)
s.NoError(err)
err = Write(filepath.Join(abs, src[1]), "File 2", 0644)
s.NoError(err)
s.NoError(Write(filepath.Join(abs, src[0]), "File 1", 0644))
s.NoError(Write(filepath.Join(abs, src[1]), "File 2", 0644))
err = Compress(abs, src, filepath.Join(abs, "uncompress_test.zip"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "uncompress_test.bz2"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "uncompress_test.tar"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "uncompress_test.tar.gz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "uncompress_test.tgz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "uncompress_test.xz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "uncompress_test.7z"))
s.NoError(err)
err = UnCompress(filepath.Join(abs, "uncompress_test.zip"), filepath.Join(abs, "uncompressed_zip"))
s.NoError(err)
data, err := Read("testdata/uncompressed_zip/uncompress_test1.txt")
s.NoError(err)
s.Equal("File 1", data)
data, err = Read("testdata/uncompressed_zip/uncompress_test2.txt")
s.NoError(err)
s.Equal("File 2", data)
err = UnCompress(filepath.Join(abs, "uncompress_test.bz2"), filepath.Join(abs, "uncompressed_bz2"))
s.NoError(err)
data, err = Read("testdata/uncompressed_bz2/uncompress_test1.txt")
s.NoError(err)
s.Equal("File 1", data)
data, err = Read("testdata/uncompressed_bz2/uncompress_test2.txt")
s.NoError(err)
s.Equal("File 2", data)
err = UnCompress(filepath.Join(abs, "uncompress_test.tar"), filepath.Join(abs, "uncompressed_tar"))
s.NoError(err)
data, err = Read("testdata/uncompressed_tar/uncompress_test1.txt")
s.NoError(err)
s.Equal("File 1", data)
data, err = Read("testdata/uncompressed_tar/uncompress_test2.txt")
s.NoError(err)
s.Equal("File 2", data)
err = UnCompress(filepath.Join(abs, "uncompress_test.tar.gz"), filepath.Join(abs, "uncompressed_tar_gz"))
s.NoError(err)
data, err = Read("testdata/uncompressed_tar_gz/uncompress_test1.txt")
s.NoError(err)
s.Equal("File 1", data)
data, err = Read("testdata/uncompressed_tar_gz/uncompress_test2.txt")
s.NoError(err)
s.Equal("File 2", data)
err = UnCompress(filepath.Join(abs, "uncompress_test.tgz"), filepath.Join(abs, "uncompressed_tgz"))
s.NoError(err)
data, err = Read("testdata/uncompressed_tgz/uncompress_test1.txt")
s.NoError(err)
s.Equal("File 1", data)
data, err = Read("testdata/uncompressed_tgz/uncompress_test2.txt")
s.NoError(err)
s.Equal("File 2", data)
err = UnCompress(filepath.Join(abs, "uncompress_test.xz"), filepath.Join(abs, "uncompressed_xz"))
s.NoError(err)
data, err = Read("testdata/uncompressed_xz/uncompress_test1.txt")
s.NoError(err)
s.Equal("File 1", data)
data, err = Read("testdata/uncompressed_xz/uncompress_test2.txt")
s.NoError(err)
s.Equal("File 2", data)
err = UnCompress(filepath.Join(abs, "uncompress_test.7z"), filepath.Join(abs, "uncompressed_7z"))
s.NoError(err)
data, err = Read("testdata/uncompressed_7z/uncompress_test1.txt")
s.NoError(err)
s.Equal("File 1", data)
data, err = Read("testdata/uncompressed_7z/uncompress_test2.txt")
s.NoError(err)
s.Equal("File 2", data)
s.NoError(Remove("testdata"))
for _, ext := range archiveExts {
dst := filepath.Join(abs, "uncompressed"+strings.ReplaceAll(ext, ".", "_"))
s.NoError(Compress(abs, src, filepath.Join(abs, "uncompress_test"+ext)), ext)
s.NoError(UnCompress(filepath.Join(abs, "uncompress_test"+ext), dst), ext)
data, err := Read(filepath.Join(dst, src[0]))
s.NoError(err, ext)
s.Equal("File 1", data, ext)
data, err = Read(filepath.Join(dst, src[1]))
s.NoError(err, ext)
s.Equal("File 2", data, ext)
}
// 单文件压缩格式解压后去掉压缩后缀恢复原文件名
for _, ext := range singleExts {
dst := filepath.Join(abs, "uncompressed_single"+strings.ReplaceAll(ext, ".", "_"))
s.NoError(Compress(abs, src[:1], filepath.Join(abs, src[0]+ext)), ext)
s.NoError(UnCompress(filepath.Join(abs, src[0]+ext), dst), ext)
data, err := Read(filepath.Join(dst, src[0]))
s.NoError(err, ext)
s.Equal("File 1", data, ext)
}
}
func (s *IOTestSuite) TestListCompress() {
abs, err := filepath.Abs("testdata")
s.NoError(err)
src := []string{"list_archive_test1.txt", "list_archive_test2.txt"}
err = Write(filepath.Join(abs, src[0]), "File 1", 0644)
s.NoError(err)
err = Write(filepath.Join(abs, src[1]), "File 2", 0644)
s.NoError(err)
s.NoError(Write(filepath.Join(abs, src[0]), "File 1", 0644))
s.NoError(Write(filepath.Join(abs, src[1]), "File 2", 0644))
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.zip"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.bz2"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.tar"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.tar.gz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.tgz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.xz"))
s.NoError(err)
err = Compress(abs, src, filepath.Join(abs, "list_archive_test.7z"))
s.NoError(err)
list, err := ListCompress(filepath.Join(abs, "list_archive_test.zip"))
s.NoError(err)
s.Len(list, 2)
list, err = ListCompress(filepath.Join(abs, "list_archive_test.bz2"))
s.NoError(err)
s.Len(list, 2)
list, err = ListCompress(filepath.Join(abs, "list_archive_test.tar"))
s.NoError(err)
s.Len(list, 2)
list, err = ListCompress(filepath.Join(abs, "list_archive_test.tar.gz"))
s.NoError(err)
s.Len(list, 2)
list, err = ListCompress(filepath.Join(abs, "list_archive_test.tgz"))
s.NoError(err)
s.Len(list, 2)
list, err = ListCompress(filepath.Join(abs, "list_archive_test.xz"))
s.NoError(err)
s.Len(list, 2)
list, err = ListCompress(filepath.Join(abs, "list_archive_test.7z"))
s.NoError(err)
s.Len(list, 2)
s.NoError(Remove("testdata"))
for _, ext := range archiveExts {
s.NoError(Compress(abs, src, filepath.Join(abs, "list_archive_test"+ext)), ext)
list, err := ListCompress(filepath.Join(abs, "list_archive_test"+ext))
s.NoError(err, ext)
s.Len(list, 2, ext)
}
// 单文件压缩格式返回去掉压缩后缀的文件名
for _, ext := range singleExts {
s.NoError(Compress(abs, src[:1], filepath.Join(abs, src[0]+ext)), ext)
list, err := ListCompress(filepath.Join(abs, src[0]+ext))
s.NoError(err, ext)
s.Equal([]string{src[0]}, list, ext)
}
}
func (s *IOTestSuite) TestRemoveDeletesFileOrDirectory() {