Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allows application/jar files to be delivered by postal #196

Merged
merged 2 commits into from
Jul 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion postal/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ func (s Service) Deliver(dependency Dependency, cnbPath, layerPath, platformPath

validatedReader := cargo.NewValidatedReader(bundle, dependency.SHA256)

err = vacation.NewArchive(validatedReader).StripComponents(dependency.StripComponents).Decompress(layerPath)
name := filepath.Base(dependency.URI)
err = vacation.NewArchive(validatedReader).WithName(name).StripComponents(dependency.StripComponents).Decompress(layerPath)
if err != nil {
return err
}
Expand Down
49 changes: 49 additions & 0 deletions postal/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ version = "this is super not semver"
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(os.FileMode(0755)))
})

context("when the dependency has a strip-components value set", func() {
it.Before(func() {
var err error
Expand Down Expand Up @@ -484,7 +485,55 @@ version = "this is super not semver"
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(os.FileMode(0755)))
})
})

context("when the dependency should be a named file", func() {
it.Before(func() {
var err error
layerPath, err = os.MkdirTemp("", "path")
Expect(err).NotTo(HaveOccurred())

buffer := bytes.NewBuffer(nil)
buffer.WriteString("some-file-contents")

sum := sha256.Sum256(buffer.Bytes())
dependencySHA = hex.EncodeToString(sum[:])

transport.DropCall.Returns.ReadCloser = io.NopCloser(buffer)

deliver = func() error {
return service.Deliver(postal.Dependency{
ID: "some-entry",
Stacks: []string{"some-stack"},
URI: "https://dependencies.example.com/dependencies/some-file-name.txt",
SHA256: dependencySHA,
Version: "1.2.3",
}, "some-cnb-path",
layerPath,
platformPath,
)
}
})

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

it("downloads the dependency and copies it into the path with the given name", func() {
err := deliver()
Expect(err).NotTo(HaveOccurred())

Expect(transport.DropCall.Receives.Root).To(Equal("some-cnb-path"))
Expect(transport.DropCall.Receives.Uri).To(Equal("https://dependencies.example.com/dependencies/some-file-name.txt"))

files, err := filepath.Glob(fmt.Sprintf("%s/*", layerPath))
Expect(err).NotTo(HaveOccurred())
Expect(files).To(ConsistOf([]string{filepath.Join(layerPath, "some-file-name.txt")}))

content, err := os.ReadFile(filepath.Join(layerPath, "some-file-name.txt"))
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("some-file-contents"))
})
})

context("when there is a dependency mapping via binding", func() {
Expand Down
89 changes: 89 additions & 0 deletions vacation/archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package vacation

import (
"bufio"
"fmt"
"io"
"path/filepath"

"github.com/gabriel-vasile/mimetype"
)

type Decompressor interface {
Decompress(destination string) error
}

// An Archive decompresses tar, gzip, xz, and bzip2 compressed tar, and zip files from
// an input stream.
type Archive struct {
reader io.Reader
components int
name string
}

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

// Decompress reads from Archive, determines the archive type of the input
// stream, and writes files into the destination specified.
//
// 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
// "artifact" 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.
bufferedReader := bufio.NewReader(a.reader)

// The number 3072 is lifted from the mimetype library and the definition of
// the constant at the time of writing this functionality is listed below.
// https://github.com/gabriel-vasile/mimetype/blob/c64c025a7c2d8d45ba57d3cebb50a1dbedb3ed7e/internal/matchers/matchers.go#L6
header, err := bufferedReader.Peek(3072)
if err != nil && err != io.EOF {
return err
}

mime := mimetype.Detect(header)

// This switch case is reponsible for determining what the decompression
// strategy should be.
var decompressor Decompressor
switch mime.String() {
case "application/x-tar":
decompressor = NewTarArchive(bufferedReader).StripComponents(a.components)
case "application/gzip":
decompressor = NewTarGzipArchive(bufferedReader).StripComponents(a.components)
case "application/x-xz":
decompressor = NewTarXZArchive(bufferedReader).StripComponents(a.components)
case "application/x-bzip2":
decompressor = NewTarBzip2Archive(bufferedReader).StripComponents(a.components)
case "application/zip":
decompressor = NewZipArchive(bufferedReader)
case "text/plain; charset=utf-8", "application/jar":
destination = filepath.Join(destination, a.name)
decompressor = NewNopArchive(bufferedReader)
default:
return fmt.Errorf("unsupported archive type: %s", mime.String())
}

return decompressor.Decompress(destination)
}

// StripComponents behaves like the --strip-components flag on tar command
// removing the first n levels from the final decompression destination.
// Setting this is a no-op for archive types that do not use --strip-components
// (such as zip).
func (a Archive) StripComponents(components int) Archive {
a.components = components
return a
}

func (a Archive) WithName(name string) Archive {
a.name = name
return a
}
95 changes: 94 additions & 1 deletion vacation/vacation_archive_test.go → vacation/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
. "github.com/onsi/gomega"
)

func testVacationArchive(t *testing.T, context spec.G, it spec.S) {
func testArchive(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
Expand Down Expand Up @@ -322,6 +322,99 @@ func testVacationArchive(t *testing.T, context spec.G, it spec.S) {
})
})

context("when passed the reader of a text file", func() {
var (
archive vacation.Archive
tempDir string
)

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

buffer := bytes.NewBuffer([]byte(`some contents`))

archive = vacation.NewArchive(buffer)
})

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

it("writes a text file onto the path", 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([]byte(`some contents`)))
})

context("when given a name", func() {
it.Before(func() {
archive = archive.WithName("some-text-file")
})

it("writes a text file onto the path with that name", func() {
err := archive.Decompress(tempDir)
Expect(err).NotTo(HaveOccurred())

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

context("when passed the reader of a jar file", func() {
var (
archive vacation.Archive
tempDir string
header []byte
)

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

// JAR header copied from https://github.com/gabriel-vasile/mimetype/blob/c4c6791c993e7f509de8ef38f149a59533e30bbc/testdata/jar.jar
header = []byte("\x50\x4b\x03\x04\x14\x00\x08\x08\x08\x00\x59\x71\xbf\x4c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x04\x00\x4d\x45\x54\x41\x2d\x49\x4e\x46\x2f\xfe\xca\x00\x00\x03\x00\x50\x4b\x07\x08\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x50\x4b\x03\x04\x14\x00\x08\x08\x08\x00\x59\x71\xbf\x4c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00\x00\x00\x4d\x45\x54\x41\x2d\x49\x4e\x46\x2f\x4d\x41\x4e\x49\x46\x45\x53\x54\x2e\x4d\x46\xf3\x4d\xcc\xcb\x4c\x4b\x2d\x2e\xd1\x0d\x4b\x2d\x2a\xce\xcc\xcf\xb3\x52\x30\xd4\x33\xe0\xe5\x72\x2e\x4a\x4d\x2c\x49\x4d\xd1\x75\xaa\x04\x09\x58\xe8\x19\xc4\x1b\x9a\x1a\x2a\x68\xf8\x17\x25\x26\xe7\xa4\x2a\x38\xe7\x17\x15\xe4\x17\x25\x96\x00\xd5\x6b\xf2\x72\xf9\x26\x66\xe6\xe9\x3a\xe7\x24\x16\x17\x5b\x29\x78\xa4\xe6\xe4\xe4\x87\xe7\x17\xe5\xa4\xf0\x72\xf1\x72\x01\x00\x50\x4b\x07\x08\x86\x7d\x5d\xeb\x5c\x00\x00\x00\x5d\x00\x00\x00\x50\x4b\x03\x04\x14\x00\x08\x08\x08\x00\x12\x71\xbf\x4c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x48\x65\x6c\x6c\x6f\x57\x6f\x72\x6c\x64\x2e\x63\x6c\x61\x73\x73\x6d\x50\x4d\x4b\xc3\x40\x10\x7d\xdb\xa6\x4d\x13\x53\x53\x5b\x53\x3f\x0b\xf6\x50\x88\x22\xe6\xe2\xad\xe2\x45\x10\x0f\x45\x85\x88\x1e\x3c\x6d\xda\xa5\x6c\xd9\x24\x12\x13\xc1\x9f\xa5\x07\x05\x0f\xfe\x00\x7f\x94\x38\x1b\x85\x20\x74\x0f\xb3\x3b\x6f\xde\x9b\x79\xb3\x5f\xdf\x1f\x9f\x00\x8e\x31\xb0\xd1\x84\x6b\xa1\x83\xb5\x16\xba\x36\x7a\x58\x37\xe1\x99\xe8\x33\x34\x4f\x64\x22\xf3\x53\x86\xba\xbf\x7f\xcb\x60\x9c\xa5\x33\xc1\xe0\x4e\x64\x22\x2e\x8b\x38\x12\xd9\x0d\x8f\x14\x21\x46\xcc\x65\xc2\xd0\xf7\xef\x27\x0b\xfe\xc4\x03\xc5\x93\x79\x10\xe6\x99\x4c\xe6\x63\x2d\xb4\xc3\xb4\xc8\xa6\xe2\x5c\x6a\xb2\x7b\x21\x94\x4a\xef\xd2\x4c\xcd\x8e\x34\xdb\x81\x89\x96\x89\x0d\x07\x9b\xd8\x62\x68\x97\xe5\xc3\xbd\x92\x30\x34\xb1\xed\x60\x07\xbb\xd4\xa3\x92\x31\x74\xaa\x31\x57\xd1\x42\x4c\xf3\x7f\x50\xf8\xfc\x98\x8b\x98\x5c\xa7\x05\x15\xbc\x5f\x4f\x32\x0d\xae\xc9\x50\x4e\xb6\x04\x8f\xc7\x0c\xbd\x25\x30\x83\xf9\xa0\x33\x45\xdb\x78\xfe\xb2\x65\x30\x44\x83\xfe\x4b\x9f\x1a\x98\xb6\x4e\xd1\xa2\x6c\x40\x37\xa3\xbb\x71\xf0\x0e\xf6\x42\x0f\xb2\x4c\xb1\x59\x82\x9a\xb2\x02\xe7\x8f\x3a\x2a\xa5\x80\xf5\x8a\x5a\xb7\xfe\x06\xa3\xa2\xdb\x54\xa2\x1e\xd4\x55\x0b\xdb\xe5\x94\xd5\x1f\x50\x4b\x07\x08\xe5\x38\x99\x3f\x21\x01\x00\x00\xab\x01\x00\x00\x50\x4b\x01\x02\x14\x00\x14\x00\x08\x08\x08\x00\x59\x71\xbf\x4c\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x09\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x4d\x45\x54\x41\x2d\x49\x4e\x46\x2f\xfe\xca\x00\x00\x50\x4b\x01\x02\x14\x00\x14\x00\x08\x08\x08\x00\x59\x71\xbf\x4c\x86\x7d\x5d\xeb\x5c\x00\x00\x00\x5d\x00\x00\x00\x14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x3d\x00\x00\x00\x4d\x45\x54\x41\x2d\x49\x4e\x46\x2f\x4d\x41\x4e\x49\x46\x45\x53\x54\x2e\x4d\x46\x50\x4b\x01\x02\x14\x00\x14\x00\x08\x08\x08\x00\x12\x71\xbf\x4c\xe5\x38\x99\x3f\x21\x01\x00\x00\xab\x01\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xdb\x00\x00\x00\x48\x65\x6c\x6c\x6f\x57\x6f\x72\x6c\x64\x2e\x63\x6c\x61\x73\x73\x50\x4b\x05\x06\x00\x00\x00\x00\x03\x00\x03\x00\xbb\x00\x00\x00\x3a\x02\x00\x00\x00\x00")
buffer := bytes.NewBuffer(header)

archive = vacation.NewArchive(buffer)
})

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

it("writes a jar file onto the path", 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(header))
})

context("when given a name", func() {
it.Before(func() {
archive = archive.WithName("some-jar-file")
})

it("writes a jar file onto the path with that name", func() {
err := archive.Decompress(tempDir)
Expect(err).NotTo(HaveOccurred())

content, err := os.ReadFile(filepath.Join(tempDir, "some-jar-file"))
Expect(err).NotTo(HaveOccurred())
Expect(content).To(Equal(header))
})
})
})

context("failure cases", func() {
context("the buffer passed is of are unknown type", func() {
var (
Expand Down
16 changes: 8 additions & 8 deletions vacation/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import (

func TestVacation(t *testing.T) {
suite := spec.New("vacation", spec.Report(report.Terminal{}))
suite("VacationArchive", testVacationArchive)
suite("VacationTarBzip2", testVacationTarBzip2)
suite("VacationSymlinkSorting", testVacationSymlinkSorting)
suite("VacationTar", testVacationTar)
suite("VacationTarGzip", testVacationTarGzip)
suite("VacationTarXZ", testVacationTarXZ)
suite("VacationText", testVacationText)
suite("VacationZip", testVacationZip)
suite("Archive", testArchive)
suite("NopArchive", testNopArchive)
suite("SymlinkSorting", testSymlinkSorting)
suite("TarArchive", testTarArchive)
suite("TarBzip2Archive", testTarBzip2Archive)
suite("TarGzipArchive", testTarGzipArchive)
suite("TarXZArchive", testTarXZArchive)
suite("ZipArchive", testZipArchive)
suite.Run(t)
}
33 changes: 33 additions & 0 deletions vacation/nop_archive.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package vacation

import (
"io"
"os"
)

// A NopArchive implements the common archive interface, but acts as a no-op,
// simply copying the reader to the destination.
type NopArchive struct {
reader io.Reader
}

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

// Decompress copies the reader contents into the destination specified.
func (na NopArchive) Decompress(destination string) error {
file, err := os.Create(destination)
if err != nil {
return err
}
defer file.Close()

_, err = io.Copy(file, na.reader)
if err != nil {
return err
}

return nil
}
56 changes: 56 additions & 0 deletions vacation/nop_archive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package vacation_test

import (
"bytes"
"os"
"path/filepath"
"testing"

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

. "github.com/onsi/gomega"
)

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

context("Decompress", func() {
var (
archive vacation.NopArchive
tempDir string
)

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

buffer := bytes.NewBuffer([]byte(`some contents`))

archive = vacation.NewNopArchive(buffer)
})

it.After(func() {
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"))
Expect(err).NotTo(HaveOccurred())

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

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")))
})
})
})
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
. "github.com/onsi/gomega"
)

func testVacationSymlinkSorting(t *testing.T, context spec.G, it spec.S) {
func testSymlinkSorting(t *testing.T, context spec.G, it spec.S) {
var (
Expect = NewWithT(t).Expect
)
Expand Down
Loading