Skip to content

Commit

Permalink
Makes api changes to Executable and NopArchive
Browse files Browse the repository at this point in the history
- NopArchive will now no longer write directly to the destination if a
name is not given becuase it now set a default name (`artifact`). If you
were using the generic Archive type you will see no difference but those
who were using the type directly must now specify a directory as the
destination, not a file name.
- NewExecutable now conforms to the same api as the other archive
instantiation functions excepting only a reader and allowing individuals
to set the file name using a `WithName()` option with mirrors the
behavior of all other archive objects.
-Executable now has a default file name (`artifact`) if you were using
the generic Archive type you will see no difference however becuase the
Executable instantiation function no longer requires a name a default is
now assigned on instantiation the same way it is done for the
NopArchive.
- Excutable no longer places the executable in a directory name `bin`
nor does it create all directories in the destination path. I felt that
automatically placing the executable in a `bin` directory felt to
opinionated, I think that it is more than reasonable for a user to want
to download and executable and not want to have it placed in a directory
named `bin` and this default behavior hinders that user. I think that it
is more than reasonable to expect that the destination given is a `bin`
directory if that is the desired effect. As for no longer creating all
of directories in the destination path, NopArchive is precedent setting
as it expects to write files only to destinations that exist already.
  • Loading branch information
ForestEckhardt authored and ryanmoran committed Dec 6, 2021
1 parent 879f5f0 commit 9e15703
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 39 deletions.
14 changes: 5 additions & 9 deletions vacation/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bufio"
"fmt"
"io"
"path/filepath"

"github.com/gabriel-vasile/mimetype"
)
Expand All @@ -25,7 +24,6 @@ type Archive struct {
func NewArchive(inputReader io.Reader) Archive {
return Archive{
reader: inputReader,
name: "artifact",
}
}

Expand All @@ -34,8 +32,7 @@ func NewArchive(inputReader io.Reader) Archive {
//
// Archive decompression will also handle files that are types "text/plain;
// charset=utf-8" and write the contents of the input stream to a file name
// specified by the `Archive.WithName()` option (or defaults to "artifact")
// in the destination directory.
// specified by the `Archive.WithName()` option in the destination directory.
func (a Archive) Decompress(destination string) error {
// Convert reader into a buffered read so that the header can be peeked to
// determine the type.
Expand All @@ -60,16 +57,15 @@ func (a Archive) Decompress(destination string) error {
case "application/gzip":
decompressor = NewGzipArchive(bufferedReader).StripComponents(a.components).WithName(a.name)
case "application/x-xz":
decompressor = NewXZArchive(bufferedReader).StripComponents(a.components)
decompressor = NewXZArchive(bufferedReader).StripComponents(a.components).WithName(a.name)
case "application/x-bzip2":
decompressor = NewBzip2Archive(bufferedReader).StripComponents(a.components)
decompressor = NewBzip2Archive(bufferedReader).StripComponents(a.components).WithName(a.name)
case "application/zip":
decompressor = NewZipArchive(bufferedReader).StripComponents(a.components)
case "application/x-executable":
decompressor = NewExecutable(bufferedReader, a.name)
decompressor = NewExecutable(bufferedReader).WithName(a.name)
case "text/plain; charset=utf-8", "application/jar":
destination = filepath.Join(destination, a.name)
decompressor = NewNopArchive(bufferedReader)
decompressor = NewNopArchive(bufferedReader).WithName(a.name)
default:
return fmt.Errorf("unsupported archive type: %s", mime.String())
}
Expand Down
14 changes: 4 additions & 10 deletions vacation/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,6 @@ func testArchive(t *testing.T, context spec.G, it spec.S) {
// Encoding of a very small elf executable from https://github.com/mathiasbynens/small
encodedContents = []byte(`f0VMRgEBAQAAAAAAAAAAAAIAAwABAAAAGUDNgCwAAAAAAAAAAAAAADQAIAABAAAAAAAAAABAzYAAQM2ATAAAAEwAAAAFAAAAABAAAA==`)
literalContents []byte
fileName = "exe"
)

it.Before(func() {
Expand All @@ -367,27 +366,22 @@ func testArchive(t *testing.T, context spec.G, it spec.S) {
literalContents, err = ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(encodedContents)))
Expect(err).NotTo(HaveOccurred())

archive = vacation.NewArchive(bytes.NewBuffer(literalContents)).WithName(fileName)
archive = vacation.NewArchive(bytes.NewBuffer(literalContents))
})

it.After(func() {
Expect(os.RemoveAll(tempDir)).To(Succeed())
})

it("writes the executable in the bin dir", func() {
it("writes the executable in the destination directory and sets the permissions", func() {
err := archive.Decompress(tempDir)
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(tempDir, "bin", fileName))
content, err := os.ReadFile(filepath.Join(tempDir, "artifact"))
Expect(err).NotTo(HaveOccurred())
Expect(content).To(Equal(literalContents))
})

it("gives the executable execute permission", func() {
err := archive.Decompress(tempDir)
Expect(err).NotTo(HaveOccurred())

info, err := os.Stat(filepath.Join(tempDir, "bin", fileName))
info, err := os.Stat(filepath.Join(tempDir, "artifact"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(fs.FileMode(0755)))
})
Expand Down
38 changes: 25 additions & 13 deletions vacation/executable.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,26 @@ import (
"path/filepath"
)

// An Executable writes an executable files from an input stream to the bin/ directory.
// An Executable writes an executable files from an input stream to the with a
// file name specified by the option `Executable.WithName()` (or defaults to
// `artifact`) in the destination directory with executable permissions (0755).
type Executable struct {
reader io.Reader
name string
reader io.Reader
name string
}

func NewExecutable(inputReader io.Reader, name string) Executable {
return Executable{reader: inputReader, name: name}
// NewExecutable returns a new Executable that reads from inputReader.
func NewExecutable(inputReader io.Reader) Executable {
return Executable{
reader: inputReader,
name: "artifact",
}
}

// Decompress copies the reader contents into the destination specified and
// sets executable permissions.
func (e Executable) Decompress(destination string) error {
err := os.MkdirAll(filepath.Join(destination, "bin"), 0755)
if err != nil {
return err
}

file, err := os.Create(filepath.Join(destination, "bin", e.name))
file, err := os.Create(filepath.Join(destination, e.name))
if err != nil {
return err
}
Expand All @@ -33,10 +36,19 @@ func (e Executable) Decompress(destination string) error {
return err
}

err = os.Chmod(filepath.Join(destination, "bin", e.name), 0755)
err = os.Chmod(filepath.Join(destination, e.name), 0755)
if err != nil {
return err
}

return nil
}
}

// WithName provides a way of overriding the name of the file
// that the decompressed file will be copied into.
func (e Executable) WithName(name string) Executable {
if name != "" {
e.name = name
}
return e
}
82 changes: 82 additions & 0 deletions vacation/executable_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package vacation_test

import (
"bytes"
"encoding/base64"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"testing"

"github.com/paketo-buildpacks/packit/vacation"
"github.com/sclevine/spec"

. "github.com/onsi/gomega"
)

func testExecutable(t *testing.T, context spec.G, it spec.S) {
var Expect = NewWithT(t).Expect

context("Decompress", func() {
var (
archive vacation.Executable
tempDir string
// Encoding of a very small elf executable from https://github.com/mathiasbynens/small
encodedContents = []byte(`f0VMRgEBAQAAAAAAAAAAAAIAAwABAAAAGUDNgCwAAAAAAAAAAAAAADQAIAABAAAAAAAAAABAzYAAQM2ATAAAAEwAAAAFAAAAABAAAA==`)
literalContents []byte
)

it.Before(func() {
var err error
tempDir, err = os.MkdirTemp("", "vacation")
Expect(err).NotTo(HaveOccurred())

literalContents, err = ioutil.ReadAll(base64.NewDecoder(base64.StdEncoding, bytes.NewBuffer(encodedContents)))
Expect(err).NotTo(HaveOccurred())

archive = vacation.NewExecutable(bytes.NewBuffer(literalContents))
})

it.After(func() {
Expect(os.RemoveAll(tempDir)).To(Succeed())
})

context("when passed the reader of an executable file", func() {
it("writes the executable in the destination directory and sets the permissions using a default name", func() {
err := archive.Decompress(tempDir)
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(tempDir, "artifact"))
Expect(err).NotTo(HaveOccurred())
Expect(content).To(Equal(literalContents))

info, err := os.Stat(filepath.Join(tempDir, "artifact"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(fs.FileMode(0755)))
})

it("writes the executable in the destination directory and sets the permissions using a given name", func() {
err := archive.WithName("executable").Decompress(tempDir)
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(tempDir, "executable"))
Expect(err).NotTo(HaveOccurred())
Expect(content).To(Equal(literalContents))

info, err := os.Stat(filepath.Join(tempDir, "executable"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(fs.FileMode(0755)))
})
})

context("failure cases", func() {
context("when the destination file cannot be created", func() {
it("returns an error", func() {
err := archive.Decompress("/no/such/path")
Expect(err).To(MatchError(ContainSubstring("no such file or directory")))
})
})
})
})
}
5 changes: 3 additions & 2 deletions vacation/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import (
func TestVacation(t *testing.T) {
suite := spec.New("vacation", spec.Report(report.Terminal{}))
suite("Archive", testArchive)
suite("Bzip2Archive", testBzip2Archive)
suite("Executable", testExecutable)
suite("GzipArchive", testGzipArchive)
suite("LinkSorting", testLinkSorting)
suite("NopArchive", testNopArchive)
suite("TarArchive", testTarArchive)
suite("Bzip2Archive", testBzip2Archive)
suite("GzipArchive", testGzipArchive)
suite("XZArchive", testXZArchive)
suite("ZipArchive", testZipArchive)
suite.Run(t)
Expand Down
22 changes: 19 additions & 3 deletions vacation/nop_archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,29 @@ package vacation
import (
"io"
"os"
"path/filepath"
)

// A NopArchive implements the common archive interface, but acts as a no-op,
// simply copying the reader to the destination.
// simply copying the reader to the destination with a file name specified by
// the option `NopArchive.WithName()` (or defaults to `artifact`) in the
// destination directory.
type NopArchive struct {
reader io.Reader
name string
}

// NewNopArchive returns a new NopArchive
func NewNopArchive(r io.Reader) NopArchive {
return NopArchive{reader: r}
return NopArchive{
reader: r,
name: "artifact",
}
}

// Decompress copies the reader contents into the destination specified.
func (na NopArchive) Decompress(destination string) error {
file, err := os.Create(destination)
file, err := os.Create(filepath.Join(destination, na.name))
if err != nil {
return err
}
Expand All @@ -31,3 +38,12 @@ func (na NopArchive) Decompress(destination string) error {

return nil
}

// WithName provides a way of overriding the name of the file
// that the decompressed file will be copied into.
func (na NopArchive) WithName(name string) NopArchive {
if name != "" {
na.name = name
}
return na
}
13 changes: 11 additions & 2 deletions vacation/nop_archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,17 @@ func testNopArchive(t *testing.T, context spec.G, it spec.S) {
Expect(os.RemoveAll(tempDir)).To(Succeed())
})

it("copies the contents of the reader to the destination", func() {
err := archive.Decompress(filepath.Join(tempDir, "some-file"))
it("copies the contents of the reader to the destination with a default name", func() {
err := archive.Decompress(filepath.Join(tempDir))
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(tempDir, "artifact"))
Expect(err).NotTo(HaveOccurred())
Expect(content).To(Equal([]byte(`some contents`)))
})

it("copies the contents of the reader to the destination with a given name", func() {
err := archive.WithName("some-file").Decompress(filepath.Join(tempDir))
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(tempDir, "some-file"))
Expand Down

0 comments on commit 9e15703

Please sign in to comment.