diff --git a/cargo/directory_duplicator.go b/cargo/directory_duplicator.go index 0cd8844d..eeda0f8c 100644 --- a/cargo/directory_duplicator.go +++ b/cargo/directory_duplicator.go @@ -1,11 +1,6 @@ package cargo -import ( - "fmt" - "io" - "os" - "path/filepath" -) +import "github.com/paketo-buildpacks/packit/fs" type DirectoryDuplicator struct{} @@ -13,47 +8,6 @@ func NewDirectoryDuplicator() DirectoryDuplicator { return DirectoryDuplicator{} } -func (d DirectoryDuplicator) Duplicate(source, sink string) error { - return filepath.Walk(source, func(path string, info os.FileInfo, err error) error { - if err != nil { - return fmt.Errorf("source dir does not exist: %s", err) - } - - relPath, err := filepath.Rel(source, path) - if err != nil { - return fmt.Errorf("failed to get relative path: %s", err) - } - - destPath := filepath.Join(sink, relPath) - if info.IsDir() { - err := os.MkdirAll(destPath, info.Mode()) - if err != nil { - return fmt.Errorf("duplicate error creating dir: %s", err) - } - } else if os.ModeType&info.Mode() == 0 { - src, err := os.Open(path) - if err != nil { - return fmt.Errorf("opening source file failed: %s", err) - } - defer src.Close() - - srcInfo, err := src.Stat() - if err != nil { - return fmt.Errorf("unable to stat source file: %s", err) - } - - dst, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, srcInfo.Mode()) - if err != nil { - return fmt.Errorf("duplicate error creating file: %s", err) - } - defer dst.Close() - - _, err = io.Copy(dst, src) - if err != nil { - return fmt.Errorf("copy dst to src failed: %s", err) - } - - } - return nil - }) +func (d DirectoryDuplicator) Duplicate(source, destination string) error { + return fs.Copy(source, destination) } diff --git a/cargo/directory_duplicator_test.go b/cargo/directory_duplicator_test.go index 63821523..9e50ba60 100644 --- a/cargo/directory_duplicator_test.go +++ b/cargo/directory_duplicator_test.go @@ -30,6 +30,7 @@ func testDirectoryDuplicator(t *testing.T, context spec.G, it spec.S) { Expect(os.MkdirAll(filepath.Join(sourceDir, "some-dir"), os.ModePerm)).To(Succeed()) Expect(ioutil.WriteFile(filepath.Join(sourceDir, "some-dir", "other-file"), []byte("other content"), 0755)).To(Succeed()) + Expect(os.Symlink(filepath.Join(sourceDir, "some-dir", "other-file"), filepath.Join(sourceDir, "some-dir", "link"))).To(Succeed()) destDir, err = ioutil.TempDir("", "dest") Expect(err).NotTo(HaveOccurred()) @@ -75,6 +76,14 @@ func testDirectoryDuplicator(t *testing.T, context spec.G, it spec.S) { Expect(info.Mode()).To(Equal(os.FileMode(0755))) Expect(file.Close()).To(Succeed()) + + info, err = os.Lstat(filepath.Join(destDir, "some-dir", "link")) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode() & os.ModeSymlink).To(Equal(os.ModeSymlink)) + + path, err := os.Readlink(filepath.Join(destDir, "some-dir", "link")) + Expect(err).NotTo(HaveOccurred()) + Expect(path).To(Equal(filepath.Join(destDir, "some-dir", "other-file"))) }) }) @@ -82,7 +91,7 @@ func testDirectoryDuplicator(t *testing.T, context spec.G, it spec.S) { context("source dir does not exist", func() { it("fails", func() { err := directoryDup.Duplicate("does-not-exist", destDir) - Expect(err).To(MatchError(ContainSubstring("source dir does not exist: "))) + Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) }) }) @@ -97,44 +106,7 @@ func testDirectoryDuplicator(t *testing.T, context spec.G, it spec.S) { it("fails", func() { err := directoryDup.Duplicate(sourceDir, destDir) - Expect(err).To(MatchError(ContainSubstring("opening source file failed:"))) - }) - }) - - context("when destination directory bad permissions", func() { - context("when creating dir", func() { - it.Before(func() { - Expect(os.Chmod(destDir, 0000)).To(Succeed()) - }) - - it.After(func() { - Expect(os.Chmod(destDir, os.ModePerm)).To(Succeed()) - }) - - it("fails", func() { - err := directoryDup.Duplicate(sourceDir, destDir) - Expect(err).To(MatchError(ContainSubstring("duplicate error creating dir:"))) - Expect(err).To(MatchError(ContainSubstring("permission denied"))) - }) - }) - - context("when creating file", func() { - var dirPath string - - it.Before(func() { - dirPath = filepath.Join(destDir, "some-dir") - Expect(os.MkdirAll(dirPath, 0000)).To(Succeed()) - }) - - it.After(func() { - Expect(os.Chmod(dirPath, os.ModePerm)).To(Succeed()) - }) - - it("fails", func() { - err := directoryDup.Duplicate(sourceDir, destDir) - Expect(err).To(MatchError(ContainSubstring("duplicate error creating file:"))) - Expect(err).To(MatchError(ContainSubstring("permission denied"))) - }) + Expect(err).To(MatchError(ContainSubstring("permission denied"))) }) }) }) diff --git a/cargo/file_bundler.go b/cargo/file_bundler.go index 86b3a926..a8872ede 100644 --- a/cargo/file_bundler.go +++ b/cargo/file_bundler.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "time" ) @@ -15,6 +16,7 @@ type File struct { Name string Info os.FileInfo + Link string } type FileInfo struct { @@ -81,18 +83,32 @@ func (b FileBundler) Bundle(root string, paths []string, config Config) ([]File, file.Info = NewFileInfo("buildpack.toml", buf.Len(), 0644, time.Now()) default: - fd, err := os.Open(filepath.Join(root, path)) - if err != nil { - return nil, fmt.Errorf("error opening included file: %s", err) - } - - info, err := fd.Stat() + var err error + file.Info, err = os.Lstat(filepath.Join(root, path)) if err != nil { return nil, fmt.Errorf("error stating included file: %s", err) } - file.ReadCloser = fd - file.Info = info + if file.Info.Mode()&os.ModeType != 0 { + link, err := os.Readlink(filepath.Join(root, path)) + if err != nil { + return nil, fmt.Errorf("error readlinking included file: %s", err) + } + + if !strings.HasPrefix(link, string(filepath.Separator)) { + link = filepath.Clean(filepath.Join(root, link)) + } + + file.Link, err = filepath.Rel(root, link) + if err != nil { + return nil, fmt.Errorf("error finding relative link path: %s", err) + } + } else { + file.ReadCloser, err = os.Open(filepath.Join(root, path)) + if err != nil { + return nil, fmt.Errorf("error opening included file: %s", err) + } + } } files = append(files, file) diff --git a/cargo/file_bundler_test.go b/cargo/file_bundler_test.go index e0af77a1..b444e8d0 100644 --- a/cargo/file_bundler_test.go +++ b/cargo/file_bundler_test.go @@ -26,7 +26,7 @@ func testFileBundler(t *testing.T, context spec.G, it spec.S) { context("Bundle", func() { it("returns a list of cargo files", func() { - files, err := fileBundler.Bundle(filepath.Join("jam", "testdata", "example-cnb"), []string{"bin/build", "bin/detect", "buildpack.toml"}, cargo.Config{ + files, err := fileBundler.Bundle(filepath.Join("jam", "testdata", "example-cnb"), []string{"bin/build", "bin/detect", "bin/link", "buildpack.toml"}, cargo.Config{ API: "0.2", Buildpack: cargo.ConfigBuildpack{ ID: "other-buildpack-id", @@ -37,6 +37,7 @@ func testFileBundler(t *testing.T, context spec.G, it spec.S) { IncludeFiles: []string{ "bin/build", "bin/detect", + "bin/link", "buildpack.toml", }, PrePackage: "some-pre-package-script.sh", @@ -44,11 +45,12 @@ func testFileBundler(t *testing.T, context spec.G, it spec.S) { }) Expect(err).NotTo(HaveOccurred()) - Expect(files).To(HaveLen(3)) + Expect(files).To(HaveLen(4)) Expect(files[0].Name).To(Equal("bin/build")) Expect(files[0].Info.Size()).To(Equal(int64(14))) Expect(files[0].Info.Mode()).To(Equal(os.FileMode(0755))) + Expect(files[0].Link).To(Equal("")) content, err := ioutil.ReadAll(files[0]) Expect(err).NotTo(HaveOccurred()) @@ -57,16 +59,24 @@ func testFileBundler(t *testing.T, context spec.G, it spec.S) { Expect(files[1].Name).To(Equal("bin/detect")) Expect(files[1].Info.Size()).To(Equal(int64(15))) Expect(files[1].Info.Mode()).To(Equal(os.FileMode(0755))) + Expect(files[1].Link).To(Equal("")) content, err = ioutil.ReadAll(files[1]) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal("detect-contents")) - Expect(files[2].Name).To(Equal("buildpack.toml")) - Expect(files[2].Info.Size()).To(Equal(int64(244))) - Expect(files[2].Info.Mode()).To(Equal(os.FileMode(0644))) + Expect(files[2].Name).To(Equal("bin/link")) + Expect(files[2].Info.Size()).To(Equal(int64(7))) + Expect(files[2].Info.Mode() & os.ModeSymlink).To(Equal(os.ModeSymlink)) + Expect(files[2].Link).To(Equal("build")) + Expect(files[2].ReadCloser).To(BeNil()) - content, err = ioutil.ReadAll(files[2]) + Expect(files[3].Name).To(Equal("buildpack.toml")) + Expect(files[3].Info.Size()).To(Equal(int64(256))) + Expect(files[3].Info.Mode()).To(Equal(os.FileMode(0644))) + Expect(files[3].Link).To(Equal("")) + + content, err = ioutil.ReadAll(files[3]) Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(MatchTOML(`api = "0.2" [buildpack] @@ -75,7 +85,7 @@ name = "other-buildpack-name" version = "other-buildpack-version" [metadata] -include_files = ["bin/build", "bin/detect", "buildpack.toml"] +include_files = ["bin/build", "bin/detect", "bin/link", "buildpack.toml"] pre_package = "some-pre-package-script.sh"`)) }) @@ -83,7 +93,7 @@ pre_package = "some-pre-package-script.sh"`)) context("when included file does not exist", func() { it("fails", func() { _, err := fileBundler.Bundle(filepath.Join("jam", "testdata", "example-cnb"), []string{"bin/fake/build", "bin/detect", "buildpack.toml"}, cargo.Config{}) - Expect(err).To(MatchError(ContainSubstring("error opening included file:"))) + Expect(err).To(MatchError(ContainSubstring("error stating included file:"))) }) }) }) diff --git a/cargo/jam/pack_test.go b/cargo/jam/pack_test.go index f0a37fd3..f55c0572 100644 --- a/cargo/jam/pack_test.go +++ b/cargo/jam/pack_test.go @@ -113,7 +113,6 @@ func testPack(t *testing.T, context spec.G, it spec.S) { }) context("when packaging an implementation buildpack", func() { - it.Before(func() { err := cargo.NewDirectoryDuplicator().Duplicate(filepath.Join("testdata", "example-cnb"), buildpackDir) Expect(err).NotTo(HaveOccurred()) @@ -136,6 +135,7 @@ func testPack(t *testing.T, context spec.G, it spec.S) { Expect(session.Out).To(gbytes.Say(fmt.Sprintf(" Building tarball: %s", filepath.Join(tmpDir, "output.tgz")))) Expect(session.Out).To(gbytes.Say(" bin/build")) Expect(session.Out).To(gbytes.Say(" bin/detect")) + Expect(session.Out).To(gbytes.Say(" bin/link")) Expect(session.Out).To(gbytes.Say(" buildpack.toml")) Expect(session.Out).To(gbytes.Say(" generated-file")) @@ -161,7 +161,7 @@ func testPack(t *testing.T, context spec.G, it spec.S) { homepage = "some-homepage-link" [metadata] - include_files = ["bin/build", "bin/detect", "buildpack.toml", "generated-file"] + include_files = ["bin/build", "bin/detect", "bin/link", "buildpack.toml", "generated-file"] pre_package = "./scripts/build.sh" [metadata.default-versions] some-dependency = "some-default-version" @@ -202,6 +202,12 @@ func testPack(t *testing.T, context spec.G, it spec.S) { Expect(hdr.Uname).To(Equal(userName)) Expect(hdr.Gname).To(Equal(groupName)) + _, hdr, err = ExtractFile(file, "bin/link") + Expect(err).NotTo(HaveOccurred()) + Expect(hdr.Linkname).To(Equal("bin/build")) + Expect(hdr.Uname).To(Equal(userName)) + Expect(hdr.Gname).To(Equal(groupName)) + contents, hdr, err = ExtractFile(file, "generated-file") Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("hello\n")) @@ -283,7 +289,7 @@ func testPack(t *testing.T, context spec.G, it spec.S) { homepage = "some-homepage-link" [metadata] - include_files = ["bin/build", "bin/detect", "buildpack.toml", "generated-file", "dependencies/f058c8bf6b65b829e200ef5c2d22fde0ee65b96c1fbd1b88869be133aafab64a"] + include_files = ["bin/build", "bin/detect", "bin/link", "buildpack.toml", "generated-file", "dependencies/f058c8bf6b65b829e200ef5c2d22fde0ee65b96c1fbd1b88869be133aafab64a"] pre_package = "./scripts/build.sh" [metadata.default-versions] some-dependency = "some-default-version" diff --git a/cargo/jam/testdata/example-cnb/bin/link b/cargo/jam/testdata/example-cnb/bin/link new file mode 120000 index 00000000..9581f1d5 --- /dev/null +++ b/cargo/jam/testdata/example-cnb/bin/link @@ -0,0 +1 @@ +./build \ No newline at end of file diff --git a/cargo/jam/testdata/example-cnb/buildpack.toml b/cargo/jam/testdata/example-cnb/buildpack.toml index 8cc4fb8b..add97abb 100644 --- a/cargo/jam/testdata/example-cnb/buildpack.toml +++ b/cargo/jam/testdata/example-cnb/buildpack.toml @@ -7,7 +7,7 @@ api = "0.2" homepage = "some-homepage-link" [metadata] - include_files = ["bin/build", "bin/detect", "buildpack.toml", "generated-file"] + include_files = ["bin/build", "bin/detect", "bin/link", "buildpack.toml", "generated-file"] pre_package = "./scripts/build.sh" [metadata.default-versions] some-dependency = "some-default-version" diff --git a/cargo/tar_builder.go b/cargo/tar_builder.go index 7ca25a92..19f03e89 100644 --- a/cargo/tar_builder.go +++ b/cargo/tar_builder.go @@ -60,7 +60,8 @@ func (b TarBuilder) Build(path string, files []File) error { for _, file := range files { b.logger.Subprocess(file.Name) - hdr, err := tar.FileInfoHeader(file.Info, file.Name) + + hdr, err := tar.FileInfoHeader(file.Info, file.Link) if err != nil { return fmt.Errorf("failed to create header for file %q: %w", file.Name, err) } diff --git a/cargo/tar_builder_test.go b/cargo/tar_builder_test.go index 9e77ab18..f77770cd 100644 --- a/cargo/tar_builder_test.go +++ b/cargo/tar_builder_test.go @@ -45,7 +45,7 @@ func testTarBuilder(t *testing.T, context spec.G, it spec.S) { context("Build", func() { context("given a destination and a list of files", func() { - it("constructs a tar ball", func() { + it("constructs a tarball", func() { err := builder.Build(tempFile, []cargo.File{ { Name: "buildpack.toml", @@ -62,12 +62,18 @@ func testTarBuilder(t *testing.T, context spec.G, it spec.S) { Info: cargo.NewFileInfo("detect", len("detect-contents"), 0755, time.Now()), ReadCloser: ioutil.NopCloser(strings.NewReader("detect-contents")), }, + { + Name: "bin/link", + Info: cargo.NewFileInfo("link", len("./build"), os.ModeSymlink|0755, time.Now()), + Link: "./build", + }, }) Expect(err).NotTo(HaveOccurred()) Expect(output.String()).To(ContainSubstring(fmt.Sprintf("Building tarball: %s", tempFile))) Expect(output.String()).To(ContainSubstring("bin/build")) Expect(output.String()).To(ContainSubstring("bin/detect")) + Expect(output.String()).To(ContainSubstring("bin/link")) Expect(output.String()).To(ContainSubstring("buildpack.toml")) file, err := os.Open(tempFile) @@ -93,6 +99,12 @@ func testTarBuilder(t *testing.T, context spec.G, it spec.S) { Expect(err).NotTo(HaveOccurred()) Expect(string(contents)).To(Equal("detect-contents")) Expect(hdr.Mode).To(Equal(int64(0755))) + + _, hdr, err = ExtractFile(file, "bin/link") + Expect(err).NotTo(HaveOccurred()) + Expect(hdr.Typeflag).To(Equal(byte(tar.TypeSymlink))) + Expect(hdr.Linkname).To(Equal("./build")) + Expect(hdr.Mode).To(Equal(int64(0755))) }) }) diff --git a/fs/copy.go b/fs/copy.go index e2adde20..734b2ab2 100644 --- a/fs/copy.go +++ b/fs/copy.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + "strings" ) // Copy will move a source file or directory to a destination. For directories, @@ -90,25 +91,7 @@ func copyDirectory(source, destination string) error { } case (info.Mode() & os.ModeSymlink) != 0: - link, err := os.Readlink(filepath.Join(source, path)) - if err != nil { - return err - } - - if filepath.HasPrefix(link, "..") { - link = filepath.Clean(filepath.Join(source, filepath.Base(path), link)) - } - - relativeLink, err := filepath.Rel(source, link) - if err != nil { - return err - } - - if filepath.HasPrefix(relativeLink, "..") { - err = os.Symlink(link, filepath.Join(destination, path)) - } else { - err = os.Symlink(filepath.Join(destination, relativeLink), filepath.Join(destination, path)) - } + err = copyLink(source, destination, path) if err != nil { return err } @@ -129,3 +112,34 @@ func copyDirectory(source, destination string) error { return nil } + +func copyLink(source, destination, path string) error { + link, err := os.Readlink(filepath.Join(source, path)) + if err != nil { + return err + } + + switch { + case strings.HasPrefix(link, string(filepath.Separator)): + // NOOP + + default: + link = filepath.Clean(filepath.Join(source, filepath.Dir(path), link)) + } + + relativeLink, err := filepath.Rel(source, link) + if err != nil { + return err + } + + if strings.HasPrefix(relativeLink, "..") { + err = os.Symlink(link, filepath.Join(destination, path)) + } else { + err = os.Symlink(filepath.Join(destination, relativeLink), filepath.Join(destination, path)) + } + if err != nil { + return err + } + + return nil +} diff --git a/fs/copy_test.go b/fs/copy_test.go index cfbc2cc6..6617981b 100644 --- a/fs/copy_test.go +++ b/fs/copy_test.go @@ -109,7 +109,7 @@ func testCopy(t *testing.T, context spec.G, it spec.S) { err = ioutil.WriteFile(filepath.Join(source, "some-dir", "readonly-file"), []byte("some-content"), 0444) Expect(err).NotTo(HaveOccurred()) - err = os.Symlink(filepath.Join(source, "some-dir", "some-file"), filepath.Join(source, "some-dir", "some-symlink")) + err = os.Symlink("some-file", filepath.Join(source, "some-dir", "some-symlink")) Expect(err).NotTo(HaveOccurred()) err = os.Symlink(filepath.Join(external, "some-file"), filepath.Join(source, "some-dir", "external-symlink"))