Skip to content

Commit

Permalink
Reorganized vacation package
Browse files Browse the repository at this point in the history
- introduces vacation.NopArchive type for non-archive files
- splits vacation implementation files up into type-named files
- renames vacation test files to remove stutter
- adds vacation.Archive.WithName option to allow NopArchives to have
  specified names
- uses vacation.Archive.WithName option in postal.Service.Deliver to
  deliver file to location matching the dependency URI basename
- includes application/jar mime-type as NopArchive type
  • Loading branch information
Ryan Moran authored and ForestEckhardt committed Jul 6, 2021
1 parent ec0062f commit 0de1e74
Show file tree
Hide file tree
Showing 21 changed files with 806 additions and 579 deletions.
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

0 comments on commit 0de1e74

Please sign in to comment.