Merge pull request #17 from otiai10/feature/opt-symlink

Feature/opt symlink
diff --git a/all_test.go b/all_test.go
index 4dfa389..8f41734 100644
--- a/all_test.go
+++ b/all_test.go
@@ -73,6 +73,41 @@
 		Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(0)
 	})
 
+	When(t, "symlink with Opt.OnSymlink provided", func(t *testing.T) {
+		opt := Options{OnSymlink: func(string) SymlinkAction { return Deep }}
+		err := Copy("testdata/case03", "testdata.copy/case03.deep", opt)
+		Expect(t, err).ToBe(nil)
+		info, err := os.Lstat("testdata.copy/case03.deep/case01")
+		Expect(t, err).ToBe(nil)
+		Expect(t, info.Mode()&os.ModeSymlink).ToBe(os.FileMode(0))
+
+		opt = Options{OnSymlink: func(string) SymlinkAction { return Shallow }}
+		err = Copy("testdata/case03", "testdata.copy/case03.shallow", opt)
+		Expect(t, err).ToBe(nil)
+		info, err = os.Lstat("testdata.copy/case03.shallow/case01")
+		Expect(t, err).ToBe(nil)
+		Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(os.FileMode(0))
+
+		opt = Options{OnSymlink: func(string) SymlinkAction { return Skip }}
+		err = Copy("testdata/case03", "testdata.copy/case03.skip", opt)
+		Expect(t, err).ToBe(nil)
+		_, err = os.Stat("testdata.copy/case03.skip/case01")
+		Expect(t, os.IsNotExist(err)).ToBe(true)
+
+		err = Copy("testdata/case03", "testdata.copy/case03.default")
+		Expect(t, err).ToBe(nil)
+		info, err = os.Lstat("testdata.copy/case03.default/case01")
+		Expect(t, err).ToBe(nil)
+		Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(os.FileMode(0))
+
+		opt = Options{OnSymlink: nil}
+		err = Copy("testdata/case03", "testdata.copy/case03.not-specified", opt)
+		Expect(t, err).ToBe(nil)
+		info, err = os.Lstat("testdata.copy/case03.not-specified/case01")
+		Expect(t, err).ToBe(nil)
+		Expect(t, info.Mode()&os.ModeSymlink).Not().ToBe(os.FileMode(0))
+	})
+
 	When(t, "try to copy to an existing path", func(t *testing.T) {
 		err := Copy("testdata/case03", "testdata.copy/case03")
 		Expect(t, err).Not().ToBe(nil)
diff --git a/copy.go b/copy.go
index c9f14c5..2fdb3ea 100644
--- a/copy.go
+++ b/copy.go
@@ -14,24 +14,25 @@
 	tmpPermissionForDirectory = os.FileMode(0755)
 )
 
-// Copy copies src to dest, doesn't matter if src is a directory or a file
-func Copy(src, dest string) error {
+// Copy copies src to dest, doesn't matter if src is a directory or a file.
+func Copy(src, dest string, opt ...Options) error {
+	opt = append(opt, DefaultOptions)
 	info, err := os.Lstat(src)
 	if err != nil {
 		return err
 	}
-	return copy(src, dest, info)
+	return copy(src, dest, info, opt[0])
 }
 
 // copy dispatches copy-funcs according to the mode.
 // Because this "copy" could be called recursively,
 // "info" MUST be given here, NOT nil.
-func copy(src, dest string, info os.FileInfo) error {
+func copy(src, dest string, info os.FileInfo, opt Options) error {
 	if info.Mode()&os.ModeSymlink != 0 {
-		return lcopy(src, dest, info)
+		return onsymlink(src, dest, info, opt)
 	}
 	if info.IsDir() {
-		return dcopy(src, dest, info)
+		return dcopy(src, dest, info, opt)
 	}
 	return fcopy(src, dest, info)
 }
@@ -68,7 +69,7 @@
 // dcopy is for a directory,
 // with scanning contents inside the directory
 // and pass everything to "copy" recursively.
-func dcopy(srcdir, destdir string, info os.FileInfo) (err error) {
+func dcopy(srcdir, destdir string, info os.FileInfo, opt Options) (err error) {
 
 	originalMode := info.Mode()
 
@@ -86,7 +87,7 @@
 
 	for _, content := range contents {
 		cs, cd := filepath.Join(srcdir, content.Name()), filepath.Join(destdir, content.Name())
-		if err := copy(cs, cd, content); err != nil {
+		if err := copy(cs, cd, content, opt); err != nil {
 			// If any error, exit immediately
 			return err
 		}
@@ -95,9 +96,35 @@
 	return nil
 }
 
+func onsymlink(src, dest string, info os.FileInfo, opt Options) error {
+
+	if opt.OnSymlink == nil {
+		opt.OnSymlink = DefaultOptions.OnSymlink
+	}
+
+	switch opt.OnSymlink(src) {
+	case Shallow:
+		return lcopy(src, dest)
+	case Deep:
+		orig, err := os.Readlink(src)
+		if err != nil {
+			return err
+		}
+		info, err = os.Lstat(orig)
+		if err != nil {
+			return err
+		}
+		return copy(orig, dest, info, opt)
+	case Skip:
+		fallthrough
+	default:
+		return nil // do nothing
+	}
+}
+
 // lcopy is for a symlink,
 // with just creating a new symlink by replicating src symlink.
-func lcopy(src, dest string, info os.FileInfo) error {
+func lcopy(src, dest string) error {
 	src, err := os.Readlink(src)
 	if err != nil {
 		return err
diff --git a/options.go b/options.go
new file mode 100644
index 0000000..4053a3f
--- /dev/null
+++ b/options.go
@@ -0,0 +1,26 @@
+package copy
+
+// Options specifies optional actions on copying.
+type Options struct {
+	// OnSymlink can specify what to do on symlink
+	OnSymlink func(p string) SymlinkAction
+}
+
+// SymlinkAction represents what to do on symlink.
+type SymlinkAction int
+
+const (
+	// Deep creates hard-copy of contents.
+	Deep SymlinkAction = iota
+	// Shallow creates new symlink to the dest of symlink.
+	Shallow
+	// Skip does nothing with symlink.
+	Skip
+)
+
+// DefaultOptions by default.
+var DefaultOptions = Options{
+	OnSymlink: func(string) SymlinkAction {
+		return Shallow
+	},
+}