diff --git a/go.mod b/go.mod index 67d88aa1459..0de879d7aa6 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.5.1 + github.com/ulikunitz/xz v0.5.6 github.com/xanzy/go-gitlab v0.29.0 gocloud.dev v0.19.0 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 2531a47232a..978e1ca40e6 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -7,6 +7,7 @@ import ( "github.com/goreleaser/goreleaser/pkg/archive/gzip" "github.com/goreleaser/goreleaser/pkg/archive/targz" + "github.com/goreleaser/goreleaser/pkg/archive/tarxz" "github.com/goreleaser/goreleaser/pkg/archive/zip" ) @@ -24,6 +25,9 @@ func New(file *os.File) Archive { if strings.HasSuffix(file.Name(), ".gz") { return gzip.New(file) } + if strings.HasSuffix(file.Name(), ".tar.xz") { + return tarxz.New(file) + } if strings.HasSuffix(file.Name(), ".zip") { return zip.New(file) } diff --git a/pkg/archive/archive_test.go b/pkg/archive/archive_test.go index 342c9f7cc71..6bdc67d9951 100644 --- a/pkg/archive/archive_test.go +++ b/pkg/archive/archive_test.go @@ -16,7 +16,7 @@ func TestArchive(t *testing.T) { assert.NoError(err) assert.NoError(os.Mkdir(folder+"/folder-inside", 0755)) - for _, format := range []string{"tar.gz", "zip", "gz", "willbeatargzanyway"} { + for _, format := range []string{"tar.gz", "zip", "gz", "tar.xz", "willbeatargzanyway"} { format := format t.Run(format, func(t *testing.T) { var archive = newArchive(folder, format, t) diff --git a/pkg/archive/tarxz/tarxz.go b/pkg/archive/tarxz/tarxz.go new file mode 100644 index 00000000000..21fc98a1444 --- /dev/null +++ b/pkg/archive/tarxz/tarxz.go @@ -0,0 +1,61 @@ +// Package tarxz implements the Archive interface providing tar.xz archiving +// and compression. +package tarxz + +import ( + "archive/tar" + "io" + "os" + + "github.com/ulikunitz/xz" +) + +// Archive as tar.xz +type Archive struct { + xzw *xz.Writer + tw *tar.Writer +} + +// Close all closeables +func (a Archive) Close() error { + if err := a.tw.Close(); err != nil { + return err + } + return a.xzw.Close() +} + +// New tar.xz archive +func New(target io.Writer) Archive { + xzw, _ := xz.WriterConfig{DictCap: 16 * 1024 * 1024}.NewWriter(target) + tw := tar.NewWriter(xzw) + return Archive{ + xzw: xzw, + tw: tw, + } +} + +// Add file to the archive +func (a Archive) Add(name, path string) error { + file, err := os.Open(path) // #nosec + if err != nil { + return err + } + defer file.Close() // nolint: errcheck + info, err := file.Stat() + if err != nil { + return err + } + header, err := tar.FileInfoHeader(info, name) + if err != nil { + return err + } + header.Name = name + if err = a.tw.WriteHeader(header); err != nil { + return err + } + if info.IsDir() { + return nil + } + _, err = io.Copy(a.tw, file) + return err +} diff --git a/pkg/archive/tarxz/tarxz_test.go b/pkg/archive/tarxz/tarxz_test.go new file mode 100644 index 00000000000..9b8400fde3e --- /dev/null +++ b/pkg/archive/tarxz/tarxz_test.go @@ -0,0 +1,72 @@ +package tarxz + +import ( + "archive/tar" + "io" + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/ulikunitz/xz" +) + +func TestTarXzFile(t *testing.T) { + var assert = assert.New(t) + tmp, err := ioutil.TempDir("", "") + assert.NoError(err) + f, err := os.Create(filepath.Join(tmp, "test.tar.xz")) + assert.NoError(err) + defer f.Close() // nolint: errcheck + archive := New(f) + + assert.Error(archive.Add("nope.txt", "../testdata/nope.txt")) + assert.NoError(archive.Add("foo.txt", "../testdata/foo.txt")) + assert.NoError(archive.Add("sub1", "../testdata/sub1")) + assert.NoError(archive.Add("sub1/bar.txt", "../testdata/sub1/bar.txt")) + assert.NoError(archive.Add("sub1/executable", "../testdata/sub1/executable")) + assert.NoError(archive.Add("sub1/sub2", "../testdata/sub1/sub2")) + assert.NoError(archive.Add("sub1/sub2/subfoo.txt", "../testdata/sub1/sub2/subfoo.txt")) + + assert.NoError(archive.Close()) + assert.Error(archive.Add("tar.go", "tar.go")) + assert.NoError(f.Close()) + + t.Log(f.Name()) + f, err = os.Open(f.Name()) + assert.NoError(err) + defer f.Close() // nolint: errcheck + + info, err := f.Stat() + assert.NoError(err) + assert.Truef(info.Size() < 500, "archived file should be smaller than %d", info.Size()) + + xzf, err := xz.NewReader(f) + assert.NoError(err) + //defer xzf.Close() // nolint: errcheck + + var paths []string + r := tar.NewReader(xzf) + for { + next, err := r.Next() + if err == io.EOF { + break + } + assert.NoError(err) + paths = append(paths, next.Name) + t.Logf("%s: %v", next.Name, next.FileInfo().Mode()) + if next.Name == "sub1/executable" { + var ex = next.FileInfo().Mode() | 0111 + assert.Equal(next.FileInfo().Mode().String(), ex.String()) + } + } + assert.Equal([]string{ + "foo.txt", + "sub1", + "sub1/bar.txt", + "sub1/executable", + "sub1/sub2", + "sub1/sub2/subfoo.txt", + }, paths) +}