diff --git a/pkg/io/compress.go b/pkg/io/compress.go index c2479e00..066d82fa 100644 --- a/pkg/io/compress.go +++ b/pkg/io/compress.go @@ -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) { diff --git a/pkg/io/io_test.go b/pkg/io/io_test.go index d726ba47..832d2005 100644 --- a/pkg/io/io_test.go +++ b/pkg/io/io_test.go @@ -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() {