-
Notifications
You must be signed in to change notification settings - Fork 26
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- re-uses most implementation from fs.Move
- Loading branch information
Showing
6 changed files
with
348 additions
and
115 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"))) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.