1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-09-16 09:26:52 +02:00

fix: use git-archive under the hood (#3904)

This reverts back to using `git archive` for the source archives... but
will keep supporting extra files.

##### How it works:

Basically, we run `git archive` as before.
Then, we make a backup of the generated archive, and create a new one
copying by reading from the backup and writing into the new one.
Finally, we write the extra files to the new one as well.

This only happens if the configuration does have extra files, otherwise,
just the simple `git archive` will be run.

PS: we can't just append to the archive because weird tar format
paddings et al.

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker
2023-04-07 22:53:15 -03:00
committed by GitHub
parent 57d3bdd965
commit 0eb3e7975c
16 changed files with 349 additions and 123 deletions

View File

@@ -317,10 +317,6 @@ type EnhancedArchive struct {
func (d EnhancedArchive) Add(f config.File) error { func (d EnhancedArchive) Add(f config.File) error {
name := strings.ReplaceAll(filepath.Join(d.wrap, f.Destination), "\\", "/") name := strings.ReplaceAll(filepath.Join(d.wrap, f.Destination), "\\", "/")
log.Debugf("adding file: %s as %s", f.Source, name) log.Debugf("adding file: %s as %s", f.Source, name)
if _, ok := d.files[f.Destination]; ok {
return fmt.Errorf("file %s already exists in the archive", f.Destination)
}
d.files[f.Destination] = name
ff := config.File{ ff := config.File{
Source: f.Source, Source: f.Source,
Destination: name, Destination: name,

View File

@@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/goreleaser/goreleaser/internal/testctx" "github.com/goreleaser/goreleaser/internal/testctx"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config" "github.com/goreleaser/goreleaser/pkg/config"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -30,7 +31,7 @@ func TestMeta(t *testing.T) {
require.Equal( require.Equal(
t, t,
[]string{"testdata/a/a.txt", "testdata/a/b/a.txt", "testdata/a/b/c/d.txt"}, []string{"testdata/a/a.txt", "testdata/a/b/a.txt", "testdata/a/b/c/d.txt"},
tarFiles(t, filepath.Join(dist, "foo.tar.gz")), testlib.LsArchive(t, filepath.Join(dist, "foo.tar.gz"), "tar.gz"),
) )
}) })

View File

@@ -2,10 +2,10 @@ package archive
import ( import (
"archive/tar" "archive/tar"
"archive/zip"
"compress/gzip" "compress/gzip"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -235,7 +235,7 @@ func TestRunPipe(t *testing.T) {
"foo/bar/foobar/blah.txt", "foo/bar/foobar/blah.txt",
expectBin, expectBin,
}, },
tarFiles(t, filepath.Join(dist, name)), testlib.LsArchive(t, filepath.Join(dist, name), "tar.gz"),
) )
header := tarInfo(t, filepath.Join(dist, name), expectBin) header := tarInfo(t, filepath.Join(dist, name), expectBin)
@@ -252,7 +252,7 @@ func TestRunPipe(t *testing.T) {
"foo/bar/foobar/blah.txt", "foo/bar/foobar/blah.txt",
expectBin + ".exe", expectBin + ".exe",
}, },
zipFiles(t, filepath.Join(dist, "foobar_0.0.1_windows_amd64.zip")), testlib.LsArchive(t, filepath.Join(dist, "foobar_0.0.1_windows_amd64.zip"), "zip"),
) )
} }
}) })
@@ -355,21 +355,6 @@ func TestRunPipeNoBinaries(t *testing.T) {
require.NoError(t, Pipe{}.Run(ctx)) require.NoError(t, Pipe{}.Run(ctx))
} }
func zipFiles(t *testing.T, path string) []string {
t.Helper()
f, err := os.Open(path)
require.NoError(t, err)
info, err := f.Stat()
require.NoError(t, err)
r, err := zip.NewReader(f, info.Size())
require.NoError(t, err)
paths := make([]string, len(r.File))
for i, zf := range r.File {
paths[i] = zf.Name
}
return paths
}
func tarInfo(t *testing.T, path, name string) *tar.Header { func tarInfo(t *testing.T, path, name string) *tar.Header {
t.Helper() t.Helper()
f, err := os.Open(path) f, err := os.Open(path)
@@ -391,27 +376,6 @@ func tarInfo(t *testing.T, path, name string) *tar.Header {
return nil return nil
} }
func tarFiles(t *testing.T, path string) []string {
t.Helper()
f, err := os.Open(path)
require.NoError(t, err)
defer f.Close()
gr, err := gzip.NewReader(f)
require.NoError(t, err)
defer gr.Close()
r := tar.NewReader(gr)
var paths []string
for {
next, err := r.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
paths = append(paths, next.Name)
}
return paths
}
func TestRunPipeBinary(t *testing.T) { func TestRunPipeBinary(t *testing.T) {
folder := testlib.Mktmp(t) folder := testlib.Mktmp(t)
dist := filepath.Join(folder, "dist") dist := filepath.Join(folder, "dist")
@@ -742,22 +706,11 @@ func TestRunPipeWrap(t *testing.T) {
require.Len(t, archives, 1) require.Len(t, archives, 1)
require.Equal(t, "foo_macOS", artifact.ExtraOr(*archives[0], artifact.ExtraWrappedIn, "")) require.Equal(t, "foo_macOS", artifact.ExtraOr(*archives[0], artifact.ExtraWrappedIn, ""))
// Check archive contents require.ElementsMatch(
f, err = os.Open(filepath.Join(dist, "foo.tar.gz")) t,
require.NoError(t, err) []string{"foo_macOS/README.md", "foo_macOS/mybin"},
defer func() { require.NoError(t, f.Close()) }() testlib.LsArchive(t, filepath.Join(dist, "foo.tar.gz"), "tar.gz"),
gr, err := gzip.NewReader(f) )
require.NoError(t, err)
defer func() { require.NoError(t, gr.Close()) }()
r := tar.NewReader(gr)
for _, n := range []string{"README.md", "mybin"} {
h, err := r.Next()
if err == io.EOF {
break
}
require.NoError(t, err)
require.Equal(t, filepath.Join("foo_macOS", n), h.Name)
}
} }
func TestDefault(t *testing.T) { func TestDefault(t *testing.T) {
@@ -999,10 +952,10 @@ func TestDuplicateFilesInsideArchive(t *testing.T) {
Source: ff.Name(), Source: ff.Name(),
Destination: "foo", Destination: "foo",
})) }))
require.EqualError(t, a.Add(config.File{ require.ErrorIs(t, a.Add(config.File{
Source: ff.Name(), Source: ff.Name(),
Destination: "foo", Destination: "foo",
}), "file foo already exists in the archive") }), fs.ErrExist)
} }
func TestWrapInDirectory(t *testing.T) { func TestWrapInDirectory(t *testing.T) {
@@ -1068,7 +1021,7 @@ func TestArchive_globbing(t *testing.T) {
}) })
require.NoError(t, Pipe{}.Run(ctx)) require.NoError(t, Pipe{}.Run(ctx))
require.Equal(t, append(expected, "foobin"), tarFiles(t, filepath.Join(dist, "foo.tar.gz"))) require.Equal(t, append(expected, "foobin"), testlib.LsArchive(t, filepath.Join(dist, "foo.tar.gz"), "tar.gz"))
} }
t.Run("exact src file", func(t *testing.T) { t.Run("exact src file", func(t *testing.T) {

View File

@@ -5,16 +5,15 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/caarlos0/log" "github.com/caarlos0/log"
"github.com/goreleaser/goreleaser/internal/archivefiles" "github.com/goreleaser/goreleaser/internal/archivefiles"
"github.com/goreleaser/goreleaser/internal/artifact" "github.com/goreleaser/goreleaser/internal/artifact"
"github.com/goreleaser/goreleaser/internal/deprecate" "github.com/goreleaser/goreleaser/internal/deprecate"
"github.com/goreleaser/goreleaser/internal/gio"
"github.com/goreleaser/goreleaser/internal/git" "github.com/goreleaser/goreleaser/internal/git"
"github.com/goreleaser/goreleaser/internal/tmpl" "github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/archive" "github.com/goreleaser/goreleaser/pkg/archive"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context" "github.com/goreleaser/goreleaser/pkg/context"
) )
@@ -30,49 +29,70 @@ func (Pipe) Skip(ctx *context.Context) bool {
} }
// Run the pipe. // Run the pipe.
func (Pipe) Run(ctx *context.Context) (err error) { func (Pipe) Run(ctx *context.Context) error {
format := ctx.Config.Source.Format
if format != "zip" && format != "tar" && format != "tgz" && format != "tar.gz" {
return fmt.Errorf("invalid source archive format: %s", format)
}
name, err := tmpl.New(ctx).Apply(ctx.Config.Source.NameTemplate) name, err := tmpl.New(ctx).Apply(ctx.Config.Source.NameTemplate)
if err != nil { if err != nil {
return err return err
} }
filename := name + "." + ctx.Config.Source.Format filename := name + "." + format
path := filepath.Join(ctx.Config.Dist, filename) path := filepath.Join(ctx.Config.Dist, filename)
log.WithField("file", filename).Info("creating source archive") log.WithField("file", filename).Info("creating source archive")
args := []string{
out, err := git.Run(ctx, "ls-files") "archive",
if err != nil { "-o", path,
return fmt.Errorf("could not list source files: %w", err)
} }
prefix, err := tmpl.New(ctx).Apply(ctx.Config.Source.PrefixTemplate) prefix := ""
if err != nil { if ctx.Config.Source.PrefixTemplate != "" {
pt, err := tmpl.New(ctx).Apply(ctx.Config.Source.PrefixTemplate)
if err != nil {
return err
}
prefix = pt
args = append(args, "--prefix", prefix)
}
args = append(args, ctx.Git.FullCommit)
if _, err := git.Clean(git.Run(ctx, args...)); err != nil {
return err return err
} }
af, err := os.Create(path) if len(ctx.Config.Source.Files) == 0 {
return nil
}
oldPath := path + ".bkp"
if err := gio.Copy(path, oldPath); err != nil {
return fmt.Errorf("failed make a backup of %q: %w", path, err)
}
// i could spend a lot of time trying to figure out how to append to a tar,
// tgz and zip file... but... this seems easy enough :)
of, err := os.Open(oldPath)
if err != nil { if err != nil {
return fmt.Errorf("could not create archive: %w", err) return fmt.Errorf("could not open %q: %w", oldPath, err)
}
defer of.Close()
af, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
return fmt.Errorf("could not open archive: %w", err)
} }
defer af.Close() //nolint:errcheck defer af.Close() //nolint:errcheck
arch, err := archive.New(af, ctx.Config.Source.Format) arch, err := archive.Copying(of, af, format)
if err != nil { if err != nil {
return err return err
} }
var ff []config.File
for _, f := range strings.Split(out, "\n") {
if strings.TrimSpace(f) == "" {
continue
}
ff = append(ff, config.File{
Source: f,
})
}
files, err := archivefiles.Eval( files, err := archivefiles.Eval(
tmpl.New(ctx), tmpl.New(ctx),
ctx.Config.Source.RLCP, ctx.Config.Source.RLCP,
append(ff, ctx.Config.Source.Files...), ctx.Config.Source.Files,
) )
if err != nil { if err != nil {
return err return err
@@ -96,7 +116,7 @@ func (Pipe) Run(ctx *context.Context) (err error) {
Name: filename, Name: filename,
Path: path, Path: path,
Extra: map[string]interface{}{ Extra: map[string]interface{}{
artifact.ExtraFormat: ctx.Config.Source.Format, artifact.ExtraFormat: format,
}, },
}) })
return err return err

View File

@@ -1,7 +1,6 @@
package sourcearchive package sourcearchive
import ( import (
"archive/zip"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -21,13 +20,14 @@ func TestArchive(t *testing.T) {
require.NoError(t, os.Mkdir("dist", 0o744)) require.NoError(t, os.Mkdir("dist", 0o744))
testlib.GitInit(t) testlib.GitInit(t)
require.NoError(t, os.WriteFile("code.txt", []byte("not really code"), 0o655)) require.NoError(t, os.WriteFile("code.rb", []byte("not really code"), 0o655))
require.NoError(t, os.WriteFile("code.py", []byte("print 1"), 0o655)) require.NoError(t, os.WriteFile("code.py", []byte("print 1"), 0o655))
require.NoError(t, os.WriteFile("README.md", []byte("# my dope fake project"), 0o655)) require.NoError(t, os.WriteFile("README.md", []byte("# my dope fake project"), 0o655))
testlib.GitAdd(t) testlib.GitAdd(t)
testlib.GitCommit(t, "feat: first") testlib.GitCommit(t, "feat: first")
require.NoError(t, os.WriteFile("added-later.txt", []byte("this file was added later"), 0o655)) require.NoError(t, os.WriteFile("added-later.txt", []byte("this file was added later"), 0o655))
require.NoError(t, os.WriteFile("ignored.md", []byte("never added"), 0o655)) require.NoError(t, os.WriteFile("ignored.md", []byte("never added"), 0o655))
require.NoError(t, os.WriteFile("code.txt", []byte("not really code"), 0o655))
require.NoError(t, os.MkdirAll("subfolder", 0o755)) require.NoError(t, os.MkdirAll("subfolder", 0o755))
require.NoError(t, os.WriteFile("subfolder/file.md", []byte("a file within a folder, added later"), 0o655)) require.NoError(t, os.WriteFile("subfolder/file.md", []byte("a file within a folder, added later"), 0o655))
@@ -67,17 +67,22 @@ func TestArchive(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Greater(t, stat.Size(), int64(100)) require.Greater(t, stat.Size(), int64(100))
if format != "zip" { expected := []string{
return "foo-1.0.0/",
}
require.ElementsMatch(t, []string{
"foo-1.0.0/README.md", "foo-1.0.0/README.md",
"foo-1.0.0/code.py", "foo-1.0.0/code.py",
"foo-1.0.0/code.rb",
"foo-1.0.0/code.txt", "foo-1.0.0/code.txt",
"foo-1.0.0/added-later.txt", "foo-1.0.0/added-later.txt",
"foo-1.0.0/subfolder/file.md", "foo-1.0.0/subfolder/file.md",
}, lsZip(t, path)) }
// zips wont have the parent dir
if format == "zip" {
expected = expected[1:]
}
require.ElementsMatch(t, expected, testlib.LsArchive(t, path, format))
}) })
} }
} }
@@ -93,7 +98,7 @@ func TestInvalidFormat(t *testing.T) {
}, },
}) })
require.NoError(t, Pipe{}.Default(ctx)) require.NoError(t, Pipe{}.Default(ctx))
require.EqualError(t, Pipe{}.Run(ctx), "invalid archive format: 7z") require.EqualError(t, Pipe{}.Run(ctx), "invalid source archive format: 7z")
} }
func TestDefault(t *testing.T) { func TestDefault(t *testing.T) {
@@ -112,6 +117,7 @@ func TestInvalidNameTemplate(t *testing.T) {
NameTemplate: "{{ .foo }-asdda", NameTemplate: "{{ .foo }-asdda",
}, },
}) })
require.NoError(t, Pipe{}.Default(ctx))
testlib.RequireTemplateError(t, Pipe{}.Run(ctx)) testlib.RequireTemplateError(t, Pipe{}.Run(ctx))
} }
@@ -175,20 +181,3 @@ func TestSkip(t *testing.T) {
func TestString(t *testing.T) { func TestString(t *testing.T) {
require.NotEmpty(t, Pipe{}.String()) require.NotEmpty(t, Pipe{}.String())
} }
func lsZip(tb testing.TB, path string) []string {
tb.Helper()
stat, err := os.Stat(path)
require.NoError(tb, err)
f, err := os.Open(path)
require.NoError(tb, err)
z, err := zip.NewReader(f, stat.Size())
require.NoError(tb, err)
var paths []string
for _, zf := range z.File {
paths = append(paths, zf.Name)
}
return paths
}

View File

@@ -0,0 +1,86 @@
package testlib
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"io"
"os"
"testing"
"github.com/stretchr/testify/require"
"github.com/ulikunitz/xz"
)
// LsArchive return the file list of a given archive in a given formatkj
func LsArchive(tb testing.TB, path, format string) []string {
tb.Helper()
f := openFile(tb, path)
switch format {
case "tar.gz", "tgz":
return doLsTar(openGzip(tb, f))
case "tar.xz", "txz":
return doLsTar(openXz(tb, f))
case "tar":
return doLsTar(f)
case "zip":
return lsZip(tb, f)
case "gz":
return []string{openGzip(tb, f).Header.Name}
default:
tb.Errorf("invalid format: %s", format)
return nil
}
}
func openGzip(tb testing.TB, r io.Reader) *gzip.Reader {
tb.Helper()
gz, err := gzip.NewReader(r)
require.NoError(tb, err)
return gz
}
func openXz(tb testing.TB, r io.Reader) *xz.Reader {
tb.Helper()
xz, err := xz.NewReader(r)
require.NoError(tb, err)
return xz
}
func lsZip(tb testing.TB, f *os.File) []string {
tb.Helper()
stat, err := f.Stat()
require.NoError(tb, err)
z, err := zip.NewReader(f, stat.Size())
require.NoError(tb, err)
var paths []string
for _, zf := range z.File {
paths = append(paths, zf.Name)
}
return paths
}
func doLsTar(f io.Reader) []string {
z := tar.NewReader(f)
var paths []string
for {
h, err := z.Next()
if h == nil || err == io.EOF {
break
}
if h.Format == tar.FormatPAX {
continue
}
paths = append(paths, h.Name)
}
return paths
}
func openFile(tb testing.TB, path string) *os.File {
tb.Helper()
f, err := os.Open(path)
require.NoError(tb, err)
return f
}

View File

@@ -4,6 +4,7 @@ package archive
import ( import (
"fmt" "fmt"
"io" "io"
"os"
"github.com/goreleaser/goreleaser/pkg/archive/gzip" "github.com/goreleaser/goreleaser/pkg/archive/gzip"
"github.com/goreleaser/goreleaser/pkg/archive/tar" "github.com/goreleaser/goreleaser/pkg/archive/tar"
@@ -35,3 +36,17 @@ func New(w io.Writer, format string) (Archive, error) {
} }
return nil, fmt.Errorf("invalid archive format: %s", format) return nil, fmt.Errorf("invalid archive format: %s", format)
} }
// Copying copies the source archive into a new one, which can be appended at.
// Source needs to be in the specified format.
func Copying(r *os.File, w io.Writer, format string) (Archive, error) {
switch format {
case "tar.gz", "tgz":
return targz.Copying(r, w)
case "tar":
return tar.Copying(r, w)
case "zip":
return zip.Copying(r, w)
}
return nil, fmt.Errorf("invalid archive format: %s", format)
}

View File

@@ -3,8 +3,10 @@ package archive
import ( import (
"io" "io"
"os" "os"
"path/filepath"
"testing" "testing"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config" "github.com/goreleaser/goreleaser/pkg/config"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -19,11 +21,11 @@ func TestArchive(t *testing.T) {
for _, format := range []string{"tar.gz", "zip", "gz", "tar.xz", "tar"} { for _, format := range []string{"tar.gz", "zip", "gz", "tar.xz", "tar"} {
format := format format := format
t.Run(format, func(t *testing.T) { t.Run(format, func(t *testing.T) {
archive, err := New(io.Discard, format) f1, err := os.Create(filepath.Join(t.TempDir(), "1.tar"))
require.NoError(t, err)
archive, err := New(f1, format)
require.NoError(t, err) require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, archive.Close())
})
require.NoError(t, archive.Add(config.File{ require.NoError(t, archive.Add(config.File{
Source: empty.Name(), Source: empty.Name(),
Destination: "empty.txt", Destination: "empty.txt",
@@ -32,6 +34,31 @@ func TestArchive(t *testing.T) {
Source: empty.Name() + "_nope", Source: empty.Name() + "_nope",
Destination: "dont.txt", Destination: "dont.txt",
})) }))
require.NoError(t, archive.Close())
require.NoError(t, f1.Close())
if format == "tar.xz" || format == "gz" {
_, err := Copying(f1, io.Discard, format)
require.Error(t, err)
return
}
f1, err = os.Open(f1.Name())
require.NoError(t, err)
f2, err := os.Create(filepath.Join(t.TempDir(), "2.tar"))
require.NoError(t, err)
a, err := Copying(f1, f2, format)
require.NoError(t, err)
require.NoError(t, a.Add(config.File{
Source: empty.Name(),
Destination: "added_later.txt",
}))
require.NoError(t, a.Close())
require.NoError(t, f1.Close())
require.NoError(t, f2.Close())
require.Equal(t, []string{"empty.txt", "added_later.txt"}, testlib.LsArchive(t, f2.Name(), format))
}) })
} }

View File

@@ -5,6 +5,7 @@ import (
"archive/tar" "archive/tar"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"github.com/goreleaser/goreleaser/pkg/config" "github.com/goreleaser/goreleaser/pkg/config"
@@ -12,16 +13,39 @@ import (
// Archive as tar. // Archive as tar.
type Archive struct { type Archive struct {
tw *tar.Writer tw *tar.Writer
files map[string]bool
} }
// New tar archive. // New tar archive.
func New(target io.Writer) Archive { func New(target io.Writer) Archive {
return Archive{ return Archive{
tw: tar.NewWriter(target), tw: tar.NewWriter(target),
files: map[string]bool{},
} }
} }
// Copying creates a new tar with the contents of the given tar.
func Copying(source io.Reader, target io.Writer) (Archive, error) {
w := New(target)
r := tar.NewReader(source)
for {
h, err := r.Next()
if err == io.EOF || h == nil {
break
}
w.files[h.Name] = true
if err := w.tw.WriteHeader(h); err != nil {
return w, err
}
if _, err := io.Copy(w.tw, r); err != nil {
return w, err
}
}
return w, nil
}
// Close all closeables. // Close all closeables.
func (a Archive) Close() error { func (a Archive) Close() error {
return a.tw.Close() return a.tw.Close()
@@ -29,6 +53,10 @@ func (a Archive) Close() error {
// Add file to the archive. // Add file to the archive.
func (a Archive) Add(f config.File) error { func (a Archive) Add(f config.File) error {
if _, ok := a.files[f.Destination]; ok {
return &fs.PathError{Err: fs.ErrExist, Path: f.Destination, Op: "add"}
}
a.files[f.Destination] = true
info, err := os.Lstat(f.Source) // #nosec info, err := os.Lstat(f.Source) // #nosec
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", f.Source, err) return fmt.Errorf("%s: %w", f.Source, err)

View File

@@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config" "github.com/goreleaser/goreleaser/pkg/config"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -58,6 +59,11 @@ func TestTarFile(t *testing.T) {
Destination: "link.txt", Destination: "link.txt",
})) }))
require.ErrorIs(t, archive.Add(config.File{
Source: "../testdata/regular.txt",
Destination: "link.txt",
}), fs.ErrExist)
require.NoError(t, archive.Close()) require.NoError(t, archive.Close())
require.Error(t, archive.Add(config.File{ require.Error(t, archive.Add(config.File{
Source: "tar.go", Source: "tar.go",
@@ -158,3 +164,34 @@ func TestTarInvalidLink(t *testing.T) {
Destination: "badlink.txt", Destination: "badlink.txt",
})) }))
} }
func TestCopying(t *testing.T) {
f1, err := os.Create(filepath.Join(t.TempDir(), "1.tar"))
require.NoError(t, err)
f2, err := os.Create(filepath.Join(t.TempDir(), "2.tar"))
require.NoError(t, err)
t1 := New(f1)
require.NoError(t, t1.Add(config.File{
Source: "../testdata/foo.txt",
Destination: "foo.txt",
}))
require.NoError(t, t1.Close())
require.NoError(t, f1.Close())
f1, err = os.Open(f1.Name())
require.NoError(t, err)
t2, err := Copying(f1, f2)
require.NoError(t, err)
require.NoError(t, t2.Add(config.File{
Source: "../testdata/sub1/executable",
Destination: "executable",
}))
require.NoError(t, t2.Close())
require.NoError(t, f2.Close())
require.NoError(t, f1.Close())
require.Equal(t, []string{"foo.txt"}, testlib.LsArchive(t, f1.Name(), "tar"))
require.Equal(t, []string{"foo.txt", "executable"}, testlib.LsArchive(t, f2.Name(), "tar"))
}

View File

@@ -27,6 +27,20 @@ func New(target io.Writer) Archive {
} }
} }
func Copying(source io.Reader, target io.Writer) (Archive, error) {
// the error will be nil since the compression level is valid
gw, _ := gzip.NewWriterLevel(target, gzip.BestCompression)
srcgz, err := gzip.NewReader(source)
if err != nil {
return Archive{}, err
}
tw, err := tar.Copying(srcgz, gw)
return Archive{
gw: gw,
tw: &tw,
}, err
}
// Close all closeables. // Close all closeables.
func (a Archive) Close() error { func (a Archive) Close() error {
if err := a.tw.Close(); err != nil { if err := a.tw.Close(); err != nil {

View File

@@ -7,6 +7,7 @@ import (
"compress/flate" "compress/flate"
"fmt" "fmt"
"io" "io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
@@ -15,7 +16,8 @@ import (
// Archive zip struct. // Archive zip struct.
type Archive struct { type Archive struct {
z *zip.Writer z *zip.Writer
files map[string]bool
} }
// New zip archive. // New zip archive.
@@ -25,10 +27,51 @@ func New(target io.Writer) Archive {
return flate.NewWriter(out, flate.BestCompression) return flate.NewWriter(out, flate.BestCompression)
}) })
return Archive{ return Archive{
z: compressor, z: compressor,
files: map[string]bool{},
} }
} }
// New zip archive.
func Copying(source *os.File, target io.Writer) (Archive, error) {
info, err := source.Stat()
if err != nil {
return Archive{}, err
}
r, err := zip.NewReader(source, info.Size())
if err != nil {
return Archive{}, err
}
w := New(target)
for _, zf := range r.File {
if zf.Mode().IsDir() {
continue
}
w.files[zf.Name] = true
hdr := zip.FileHeader{
Name: zf.Name,
UncompressedSize64: zf.UncompressedSize64,
UncompressedSize: zf.UncompressedSize,
CreatorVersion: zf.CreatorVersion,
ExternalAttrs: zf.ExternalAttrs,
}
ww, err := w.z.CreateHeader(&hdr)
if err != nil {
return Archive{}, fmt.Errorf("creating %q header in target: %w", zf.Name, err)
}
rr, err := zf.Open()
if err != nil {
return Archive{}, fmt.Errorf("opening %q from source: %w", zf.Name, err)
}
defer rr.Close()
if _, err = io.Copy(ww, rr); err != nil {
return Archive{}, fmt.Errorf("copy from %q source to target: %w", zf.Name, err)
}
_ = rr.Close()
}
return w, nil
}
// Close all closeables. // Close all closeables.
func (a Archive) Close() error { func (a Archive) Close() error {
return a.z.Close() return a.z.Close()
@@ -36,6 +79,10 @@ func (a Archive) Close() error {
// Add a file to the zip archive. // Add a file to the zip archive.
func (a Archive) Add(f config.File) error { func (a Archive) Add(f config.File) error {
if _, ok := a.files[f.Destination]; ok {
return &fs.PathError{Err: fs.ErrExist, Path: f.Destination, Op: "add"}
}
a.files[f.Destination] = true
info, err := os.Lstat(f.Source) // #nosec info, err := os.Lstat(f.Source) // #nosec
if err != nil { if err != nil {
return err return err

View File

@@ -59,6 +59,11 @@ func TestZipFile(t *testing.T) {
Destination: "link.txt", Destination: "link.txt",
})) }))
require.ErrorIs(t, archive.Add(config.File{
Source: "../testdata/regular.txt",
Destination: "link.txt",
}), fs.ErrExist)
require.NoError(t, archive.Close()) require.NoError(t, archive.Close())
require.Error(t, archive.Add(config.File{ require.Error(t, archive.Add(config.File{
Source: "tar.go", Source: "tar.go",

View File

@@ -933,7 +933,7 @@ type Publisher struct {
// Source configuration. // Source configuration.
type Source struct { type Source struct {
NameTemplate string `yaml:"name_template,omitempty" json:"name_template,omitempty"` NameTemplate string `yaml:"name_template,omitempty" json:"name_template,omitempty"`
Format string `yaml:"format,omitempty" json:"format,omitempty"` Format string `yaml:"format,omitempty" json:"format,omitempty" jsonschema:"enum=tar,enum=tgz,enum=tar.gz,enum=zip,default=tar.gz"`
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
PrefixTemplate string `yaml:"prefix_template,omitempty" json:"prefix_template,omitempty"` PrefixTemplate string `yaml:"prefix_template,omitempty" json:"prefix_template,omitempty"`
Files []File `yaml:"files,omitempty" json:"files,omitempty"` Files []File `yaml:"files,omitempty" json:"files,omitempty"`

View File

@@ -139,10 +139,17 @@ archives:
# #
# Default: copied from the source file # Default: copied from the source file
info: info:
# Templateable (since v1.14.0)
owner: root owner: root
# Templateable (since v1.14.0)
group: root group: root
# Must be in time.RFC3339Nano format. # Must be in time.RFC3339Nano format.
# Templateable (since v1.14.0)
mtime: '{{ .CommitDate }}' mtime: '{{ .CommitDate }}'
# File mode.
mode: 0644 mode: 0644
# Before and after hooks for each archive. # Before and after hooks for each archive.

View File

@@ -16,7 +16,8 @@ source:
name_template: '{{ .ProjectName }}' name_template: '{{ .ProjectName }}'
# Format of the archive. # Format of the archive.
# Any format git-archive supports, this supports too. #
# Valid formats are: tar, tgz, tar.gz, and zip.
# #
# Default: 'tar.gz' # Default: 'tar.gz'
format: 'tar' format: 'tar'