Skip to content

Commit

Permalink
creates a fs.Copy function, (#28)
Browse files Browse the repository at this point in the history
- re-uses most implementation from fs.Move
  • Loading branch information
dwillist authored May 27, 2020
1 parent fa610dd commit 9900e31
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 115 deletions.
131 changes: 131 additions & 0 deletions fs/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package fs

import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
)

// Copy will move a source file or directory to a destination. For directories,
// move will remap relative symlinks ensuring that they align with the
// destination directory. If the destination exists prior to invocation, it
// will be removed.
func Copy(source, destination string) error {
err := os.Remove(destination)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to copy: destination exists: %w", err)
}
}

info, err := os.Stat(source)
if err != nil {
return err
}

if info.IsDir() {
err = copyDirectory(source, destination)
if err != nil {
return err
}
} else {
err = copyFile(source, destination)
if err != nil {
return err
}
}

return nil
}

func copyFile(source, destination string) error {
sourceFile, err := os.Open(source)
if err != nil {
return err
}
defer sourceFile.Close()

destinationFile, err := os.Create(destination)
if err != nil {
return err
}
defer destinationFile.Close()

_, err = io.Copy(destinationFile, sourceFile)
if err != nil {
return err
}

info, err := sourceFile.Stat()
if err != nil {
return err
}

err = destinationFile.Chmod(info.Mode())
if err != nil {
return err
}

return nil
}

func copyDirectory(source, destination string) error {
err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}

path, err = filepath.Rel(source, path)
if err != nil {
return err
}

switch {
case info.IsDir():
err = os.Mkdir(filepath.Join(destination, path), os.ModePerm)
if err != nil {
return err
}

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))
}
if err != nil {
return err
}

default:
err = copyFile(filepath.Join(source, path), filepath.Join(destination, path))
if err != nil {
return err
}
}

return nil
})

if err != nil {
return err
}

return nil
}
211 changes: 211 additions & 0 deletions fs/copy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package fs_test

import (
"io/ioutil"
"os"
"path/filepath"
"testing"

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

. "github.com/onsi/gomega"
)

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

context("Copy", func() {
var (
sourceDir string
destinationDir string
)

it.Before(func() {
var err error
sourceDir, err = ioutil.TempDir("", "source")
Expect(err).NotTo(HaveOccurred())

destinationDir, err = ioutil.TempDir("", "destination")
Expect(err).NotTo(HaveOccurred())
})

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

context("when the source is a file", func() {
var source, destination string

it.Before(func() {
source = filepath.Join(sourceDir, "source")
destination = filepath.Join(destinationDir, "destination")

err := ioutil.WriteFile(source, []byte("some-content"), 0644)
Expect(err).NotTo(HaveOccurred())
})

it("copies the source file to the destination", func() {
err := fs.Copy(source, destination)
Expect(err).NotTo(HaveOccurred())

content, err := ioutil.ReadFile(destination)
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("some-content"))

info, err := os.Stat(destination)
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(os.FileMode(0644)))

Expect(source).To(BeAnExistingFile())
})

context("failure cases", func() {
context("when the source cannot be read", func() {
it.Before(func() {
Expect(os.Chmod(source, 0000)).To(Succeed())
})

it("returns an error", func() {
err := fs.Copy(source, destination)
Expect(err).To(MatchError(ContainSubstring("permission denied")))
})
})

context("when the destination cannot be removed", func() {
it.Before(func() {
Expect(os.Chmod(destinationDir, 0000)).To(Succeed())
})

it("returns an error", func() {
err := fs.Copy(source, destination)
Expect(err).To(MatchError(ContainSubstring("failed to copy: destination exists:")))
Expect(err).To(MatchError(ContainSubstring("permission denied")))
})
})
})
})

context("when the source is a directory", func() {
var source, destination, external string

it.Before(func() {
var err error
external, err = ioutil.TempDir("", "external")
Expect(err).NotTo(HaveOccurred())

err = ioutil.WriteFile(filepath.Join(external, "some-file"), []byte("some-content"), 0644)
Expect(err).NotTo(HaveOccurred())

source = filepath.Join(sourceDir, "source")
destination = filepath.Join(destinationDir, "destination")

Expect(os.MkdirAll(filepath.Join(source, "some-dir"), os.ModePerm)).To(Succeed())

err = ioutil.WriteFile(filepath.Join(source, "some-dir", "some-file"), []byte("some-content"), 0644)
Expect(err).NotTo(HaveOccurred())

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

err = os.Symlink(filepath.Join(external, "some-file"), filepath.Join(source, "some-dir", "external-symlink"))
Expect(err).NotTo(HaveOccurred())
})

context("when the destination does not exist", func() {
it("copies the source directory to the destination", func() {
err := fs.Copy(source, destination)
Expect(err).NotTo(HaveOccurred())

Expect(destination).To(BeADirectory())

content, err := ioutil.ReadFile(filepath.Join(destination, "some-dir", "some-file"))
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("some-content"))

info, err := os.Stat(filepath.Join(destination, "some-dir", "some-file"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(os.FileMode(0644)))

info, err = os.Stat(filepath.Join(destination, "some-dir", "readonly-file"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(os.FileMode(0444)))

path, err := os.Readlink(filepath.Join(destination, "some-dir", "some-symlink"))
Expect(err).NotTo(HaveOccurred())
Expect(path).To(Equal(filepath.Join(destination, "some-dir", "some-file")))

path, err = os.Readlink(filepath.Join(destination, "some-dir", "external-symlink"))
Expect(err).NotTo(HaveOccurred())
Expect(path).To(Equal(filepath.Join(external, "some-file")))

Expect(source).To(BeAnExistingFile())
})
})

context("when the destination is a file", func() {
it.Before(func() {
Expect(os.RemoveAll(destination))
Expect(ioutil.WriteFile(destination, []byte{}, os.ModePerm)).To(Succeed())
})

it("copies the source directory to the destination", func() {
err := fs.Copy(source, destination)
Expect(err).NotTo(HaveOccurred())

Expect(destination).To(BeADirectory())

content, err := ioutil.ReadFile(filepath.Join(destination, "some-dir", "some-file"))
Expect(err).NotTo(HaveOccurred())
Expect(string(content)).To(Equal("some-content"))

info, err := os.Stat(filepath.Join(destination, "some-dir", "some-file"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(os.FileMode(0644)))

info, err = os.Stat(filepath.Join(destination, "some-dir", "readonly-file"))
Expect(err).NotTo(HaveOccurred())
Expect(info.Mode()).To(Equal(os.FileMode(0444)))

path, err := os.Readlink(filepath.Join(destination, "some-dir", "some-symlink"))
Expect(err).NotTo(HaveOccurred())
Expect(path).To(Equal(filepath.Join(destination, "some-dir", "some-file")))

path, err = os.Readlink(filepath.Join(destination, "some-dir", "external-symlink"))
Expect(err).NotTo(HaveOccurred())
Expect(path).To(Equal(filepath.Join(external, "some-file")))

Expect(source).To(BeAnExistingFile())
})
})

context("failure cases", func() {
context("when the source does not exist", func() {
it("returns an error", func() {
err := fs.Copy("no-such-source", destination)
Expect(err).To(MatchError(ContainSubstring("no such file or directory")))
})
})

context("when the source cannot be walked", func() {
it.Before(func() {
Expect(os.Chmod(source, 0000)).To(Succeed())
})

it.After(func() {
Expect(os.Chmod(source, 0777)).To(Succeed())
})

it("returns an error", func() {
err := fs.Copy(source, destination)
Expect(err).To(MatchError(ContainSubstring("permission denied")))
})
})
})
})
})
}
1 change: 1 addition & 0 deletions fs/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func TestUnitFS(t *testing.T) {
suite := spec.New("packit/fs", spec.Report(report.Terminal{}))
suite("Move", testMove)
suite("Copy", testCopy)
suite("IsEmptyDir", testIsEmptyDir)
suite("ChecksumCalculator", testChecksumCalculator)
suite.Run(t)
Expand Down
Loading

0 comments on commit 9900e31

Please sign in to comment.